Skip to content

fix(db): create credential store with owner-only permissions - #3381

Open
elvis460 wants to merge 1 commit into
decolua:masterfrom
elvis460:fix/db-file-permissions
Open

fix(db): create credential store with owner-only permissions#3381
elvis460 wants to merge 1 commit into
decolua:masterfrom
elvis460:fix/db-file-permissions

Conversation

@elvis460

Copy link
Copy Markdown

Summary

~/.9router/db/data.sqlite is created without an explicit mode, so it inherits the process umask — 0644 on a default umask 022, i.e. world-readable. That file is the credential store:

  • providerConnections.data — provider OAuth access and refresh tokens, stored as plaintext JSON
  • apiKeys.key — client API keys, stored in plaintext (sk-…, not hashed)

Any other local account, or any process running under a different uid, can read that file and obtain full access to the linked AI provider accounts.

This is inconsistent with how the project already treats its other secrets. src/cli/api/client.js:55 writes the CLI secret with an explicit { mode: 0o600 }, and jwt-secret lands at 0600 as well. The DB — strictly more sensitive than either of those — is the one credential store that inherits the umask instead.

Reproduce

On a stock 0.5.55 install:

$ ls -l ~/.9router/db/data.sqlite
-rw-r--r--  1 user  staff  3223552  data.sqlite

$ ls -ld ~/.9router
drwxr-xr-x  12 user  staff  384  .9router

Schema-migration backups have the same problem, because backupDbLite() lets SQLite create the attached file:

$ ls -l ~/.9router/db/backups/schema-0-to-1-*/data.sqlite
-rw-r--r--  1 user  staff  634880  data.sqlite

Changes

  • src/lib/db/paths.jsensureDirs() now creates directories with 0700. Adds hardenPermissions(), which chmods the data dir, db dir and backups dir to 0700, and data.sqlite plus its -wal/-shm sidecars to 0600.
  • src/lib/db/driver.js — calls hardenPermissions() after adapter init, at the point where the DB file is guaranteed to exist.
  • src/lib/db/backup.js — migration backup dirs created 0700; backup files chmodded 0600 after copyFileSync / ATTACH.
  • src/lib/dataDir.js — a DATA_DIR supplied via env is created with 0700.

Three deliberate design points:

  1. Existing installs are repaired, not just new ones. Creating files with the right mode would only protect fresh installs — SQLite writes in place, so a DB already sitting at 0644 keeps that mode indefinitely. hardenPermissions() therefore runs on every startup and is idempotent.
  2. Windows is a no-op. chmod there is ACL-based and only the read-only bit maps through, so tightening is restricted to POSIX platforms.
  3. chmod failures are swallowed. A Docker bind mount may be owned by a different uid; failing to tighten permissions must never prevent the app from starting.

Verification

Verified against a live 0.5.55 install. After applying 0600/0700 to an existing install, a full OpenAI (codex) OAuth re-authentication completed normally and wrote a fresh providerConnections row (4064 bytes, isActive=1). File modes survived the write, since SQLite writes in place rather than recreating the file. Tightening the mode does not interfere with provider authentication — which is the obvious concern with a change like this, given that adding a provider is a write path.

Adds tests/unit/db-file-permissions.test.js covering:

  • fresh install → dirs 0700, DB 0600
  • pre-existing 0755/0644 install → repaired on startup
  • WAL sidecars → 0600
  • schema-migration backups → dir 0700, file 0600

Skipped on Windows.

Out of scope

Encrypting credentials at rest is a larger design change (key management, migration of existing rows) and is intentionally not attempted here. Filesystem permissions are the cheap first line of defence regardless of whether encryption is added later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant