|
1 | | -import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync } from "node:fs"; |
| 1 | +import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync } from "node:fs"; |
2 | 2 | import { tmpdir } from "node:os"; |
3 | 3 | import { dirname, join } from "node:path"; |
4 | 4 | import { |
@@ -40,6 +40,52 @@ export function getSchemaFenceRejection(): { |
40 | 40 |
|
41 | 41 | export const LATEST_SUPPORTED_VERSION = 41; |
42 | 42 |
|
| 43 | +// chmod is meaningless on Windows (POSIX modes are not honored), so all |
| 44 | +// permission tightening is skipped there. mkdir's `mode` is likewise ignored. |
| 45 | +const PERMISSIONS_ENFORCEABLE = process.platform !== "win32"; |
| 46 | + |
| 47 | +/** |
| 48 | + * Create `dir` (recursively) owner-only and tighten an existing dir to 0o700. |
| 49 | + * |
| 50 | + * The storage tree holds project memories, raw conversation history, and |
| 51 | + * embeddings. Created with the default umask these can be group/world-readable, |
| 52 | + * leaking that content to other local users. We create with mode 0o700 and |
| 53 | + * additionally chmod (mkdir's `mode` is masked by umask and a no-op when the |
| 54 | + * dir already exists). Best-effort: a chmod failure is logged, not fatal. |
| 55 | + */ |
| 56 | +function ensureSecureStorageDir(dir: string): void { |
| 57 | + mkdirSync(dir, { recursive: true, mode: 0o700 }); |
| 58 | + if (!PERMISSIONS_ENFORCEABLE) return; |
| 59 | + try { |
| 60 | + chmodSync(dir, 0o700); |
| 61 | + } catch (error) { |
| 62 | + log( |
| 63 | + `[magic-context] could not restrict storage dir permissions on ${dir}: ${getErrorMessage(error)}`, |
| 64 | + ); |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Restrict the SQLite DB file and its WAL/SHM sidecars to owner-only (0o600). |
| 70 | + * They are created with the process umask, which can be group/world-readable; |
| 71 | + * since they hold the same durable state as the storage dir, tighten them once |
| 72 | + * they exist. Best-effort per file. |
| 73 | + */ |
| 74 | +function restrictDatabaseFilePermissions(dbPath: string): void { |
| 75 | + if (!PERMISSIONS_ENFORCEABLE) return; |
| 76 | + for (const suffix of ["", "-wal", "-shm"]) { |
| 77 | + const file = `${dbPath}${suffix}`; |
| 78 | + if (!existsSync(file)) continue; |
| 79 | + try { |
| 80 | + chmodSync(file, 0o600); |
| 81 | + } catch (error) { |
| 82 | + log( |
| 83 | + `[magic-context] could not restrict DB file permissions on ${file}: ${getErrorMessage(error)}`, |
| 84 | + ); |
| 85 | + } |
| 86 | + } |
| 87 | +} |
| 88 | + |
43 | 89 | export interface OpenDatabaseOptions { |
44 | 90 | dbPath?: string; |
45 | 91 | latestSupportedVersion?: number; |
@@ -153,7 +199,7 @@ function migrateLegacyStorageIfNeeded(targetDbPath: string, targetDbDir: string) |
153 | 199 | log( |
154 | 200 | `[magic-context] migrating legacy plugin storage: ${legacyDir} -> ${targetDbDir} (legacy left in place as backup)`, |
155 | 201 | ); |
156 | | - mkdirSync(targetDbDir, { recursive: true }); |
| 202 | + ensureSecureStorageDir(targetDbDir); |
157 | 203 |
|
158 | 204 | // Fold the legacy WAL into the main DB FIRST so the copied target is one |
159 | 205 | // crash-consistent file. Copying .db/-wal/-shm as three separate files is |
@@ -1455,7 +1501,7 @@ export function openDatabase(dbPathOrOptions?: string | OpenDatabaseOptions): Da |
1455 | 1501 | if (!explicitDbPath) { |
1456 | 1502 | migrateLegacyStorageIfNeeded(dbPath, dbDir); |
1457 | 1503 | } |
1458 | | - mkdirSync(dbDir, { recursive: true }); |
| 1504 | + ensureSecureStorageDir(dbDir); |
1459 | 1505 |
|
1460 | 1506 | const db = new Database(dbPath); |
1461 | 1507 | if (!enforceSchemaFence(db, dbPath, latestSupportedVersion)) { |
@@ -1507,6 +1553,9 @@ export function openDatabase(dbPathOrOptions?: string | OpenDatabaseOptions): Da |
1507 | 1553 | // sidebar regression report. |
1508 | 1554 | setToolDefinitionDatabase(db); |
1509 | 1555 | loadToolDefinitionMeasurements(db); |
| 1556 | + // Tighten the DB + WAL/SHM sidecars to owner-only now that WAL mode has |
| 1557 | + // created the sidecars; best-effort, never fatal. |
| 1558 | + restrictDatabaseFilePermissions(dbPath); |
1510 | 1559 | databases.set(dbPath, db); |
1511 | 1560 | pathByDatabase.set(db, dbPath); |
1512 | 1561 | persistenceByDatabase.set(db, true); |
|
0 commit comments