// The local database sits in the same directory as `auth.json` and// `server-connections.json`, both of which are deliberately 0600. SQLite// creates its own files with the process umask instead, so on a default macOS// or Linux install the database and its WAL sidecars land at 0644 — readable// by every other account on the machine.//// These drive the real `openLocalLibsql` against a real file, because the// property under test is what the filesystem ends up holding, not what the// code appears to ask for.import { chmodSync, existsSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import type { Client } from "@libsql/client";import { createClient } from "@libsql/client";import { afterEach, describe, expect, it } from "@effect/vitest";import { openLocalLibsql } from "./libsql";const dirs: string[] = [];const clients: Client[] = [];afterEach(() => { for (const client of clients.splice(0)) client.close(); for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });});/** POSIX permission bits of `path`, or null when it does not exist. */const modeOf = (path: string): number | null => existsSync(path) ? statSync(path).mode & 0o777 : null;const tempDir = (label: string): string => { const dir = mkdtempSync(join(tmpdir(), `executor-db-perms-${label}-`)); dirs.push(dir); return dir;};/** Every file in `dir` that any account other than the owner can read. */const groupOrWorldReadable = (dir: string): readonly string[] => readdirSync(dir).filter((name) => ((modeOf(join(dir, name)) ?? 0) & 0o044) !== 0);describe("local database file permissions", () => { it("leaves nothing in the data directory readable by other accounts", async () => { const dir = tempDir("create"); const path = join(dir, "data.db"); const client = await openLocalLibsql(path); clients.push(client); // Force a write so WAL materializes its sidecars. await client.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY)"); expect(modeOf(path)).toBe(0o600); // Asserting over the whole directory rather than a fixed list of suffixes: // it does not depend on which sidecars SQLite happened to create, and it // names the offenders when it fails. expect(groupOrWorldReadable(dir)).toEqual([]); }); it("tightens a database left world-readable by an earlier version", async () => { // The upgrade path is the one that matters: a `mode` at creation cannot // help a file that already exists, which is why the fix chmods on open. const dir = tempDir("upgrade"); const path = join(dir, "data.db"); const seed = createClient({ url: `file:${path}` }); await seed.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY)"); seed.close(); // Put the file into the exact state a pre-fix install leaves behind. chmodSync(path, 0o644); expect(modeOf(path)).toBe(0o644); const client = await openLocalLibsql(path); clients.push(client); expect(modeOf(path)).toBe(0o600); expect(groupOrWorldReadable(dir)).toEqual([]); }); it("POSITIVE CONTROL: a raw libSQL open leaves the database world-readable", async () => { // Proves the assertions above can fail. Without this, a chmod that // silently stopped running would still leave them green on a machine with // a strict umask — this is the check that the check works. const dir = tempDir("control"); const path = join(dir, "data.db"); const client = createClient({ url: `file:${path}` }); await client.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY)"); client.close(); expect(modeOf(path)).not.toBe(0o600); expect(groupOrWorldReadable(dir)).toContain("data.db"); }); it("does not throw for an in-memory database", async () => { const client = await openLocalLibsql(":memory:"); clients.push(client); await expect(client.execute("SELECT 1")).resolves.toBeDefined(); });});
// The local database sits in the same directory as `auth.json` and
// `server-connections.json`, both of which are deliberately 0600. SQLite
// creates its own files with the process umask instead, so on a default macOS
// or Linux install the database and its WAL sidecars land at 0644 — readable
// by every other account on the machine.
//
// These drive the real `openLocalLibsql` against a real file, because the
// property under test is what the filesystem ends up holding, not what the
// code appears to ask for.
import { chmodSync, existsSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Client } from "@libsql/client";
import { createClient } from "@libsql/client";
import { afterEach, describe, expect, it } from "@effect/vitest";
import { openLocalLibsql } from "./libsql";
const dirs: string[] = [];
const clients: Client[] = [];
afterEach(() => {
for (const client of clients.splice(0)) client.close();
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
/** POSIX permission bits of `path`, or null when it does not exist. */
const modeOf = (path: string): number | null =>
existsSync(path) ? statSync(path).mode & 0o777 : null;
const tempDir = (label: string): string => {
const dir = mkdtempSync(join(tmpdir(), `executor-db-perms-${label}-`));
dirs.push(dir);
return dir;
};
/** Every file in `dir` that any account other than the owner can read. */
const groupOrWorldReadable = (dir: string): readonly string[] =>
readdirSync(dir).filter((name) => ((modeOf(join(dir, name)) ?? 0) & 0o044) !== 0);
describe("local database file permissions", () => {
it("leaves nothing in the data directory readable by other accounts", async () => {
const dir = tempDir("create");
const path = join(dir, "data.db");
const client = await openLocalLibsql(path);
clients.push(client);
// Force a write so WAL materializes its sidecars.
await client.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY)");
expect(modeOf(path)).toBe(0o600);
// Asserting over the whole directory rather than a fixed list of suffixes:
// it does not depend on which sidecars SQLite happened to create, and it
// names the offenders when it fails.
expect(groupOrWorldReadable(dir)).toEqual([]);
});
it("tightens a database left world-readable by an earlier version", async () => {
// The upgrade path is the one that matters: a `mode` at creation cannot
// help a file that already exists, which is why the fix chmods on open.
const dir = tempDir("upgrade");
const path = join(dir, "data.db");
const seed = createClient({ url: `file:${path}` });
await seed.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY)");
seed.close();
// Put the file into the exact state a pre-fix install leaves behind.
chmodSync(path, 0o644);
expect(modeOf(path)).toBe(0o644);
const client = await openLocalLibsql(path);
clients.push(client);
expect(modeOf(path)).toBe(0o600);
expect(groupOrWorldReadable(dir)).toEqual([]);
});
it("POSITIVE CONTROL: a raw libSQL open leaves the database world-readable", async () => {
// Proves the assertions above can fail. Without this, a chmod that
// silently stopped running would still leave them green on a machine with
// a strict umask — this is the check that the check works.
const dir = tempDir("control");
const path = join(dir, "data.db");
const client = createClient({ url: `file:${path}` });
await client.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY)");
client.close();
expect(modeOf(path)).not.toBe(0o600);
expect(groupOrWorldReadable(dir)).toContain("data.db");
});
it("does not throw for an in-memory database", async () => {
const client = await openLocalLibsql(":memory:");
clients.push(client);
await expect(client.execute("SELECT 1")).resolves.toBeDefined();
});
});