Skip to content

fix(storage): create config.db owner-only and secure database backups - #1342

Open
Dumbris wants to merge 2 commits into
mainfrom
fix/db-file-mode-0600
Open

Dumbris wants to merge 2 commits into
mainfrom
fix/db-file-mode-0600

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 22, 2026

Copy link
Copy Markdown
Member

What

~/.mcpproxy/config.db stores OAuth access/refresh tokens and DCR client secrets as plain JSON (internal/storage/models.go), but was created with mode 0644 and backed up with 0644. This PR makes the database and its backups owner-only.

  • bbolt.Open now passes 0600.
  • An existing group/world-readable database is tightened on open, clearing only the 0o077 bits so a stricter owner mode is never widened. The mode argument to bbolt.Open applies only at creation and bbolt never chmods an existing file, so this migration is the entire fix for anyone who already has a database.
  • The chmod is best-effort: stat/chmod failures are logged at debug level and never block startup.
  • Backup no longer uses Tx.CopyFile directly. CopyFile opens the destination with O_CREATE|O_TRUNC, so its mode argument is ignored when the file already exists — a chmod afterwards comes too late, since the database bytes would already have landed in whatever permissions a stale backup carried. The copy is now staged in an owner-only temp file next to the destination and renamed into place.

Why

Scoped honestly: ~/.mcpproxy is created 0700 (internal/config/loader.go), so in a default install another local user cannot traverse to the 0644 database. This is defense-in-depth, and the 0700 directory is not an invariant — the MkdirAll is skipped when DataDir still contains an unresolved ${...}, and MkdirAll never tightens an already-existing directory.

Backup currently has no production callers — it is reachable only as an exported method declared on appctx.StorageInterface. The 0644 copy was therefore a latent hazard on a public API, not a live leak.

Out of scope, deliberately:

  • cmd/mcpproxy/db_cmd.go compact/repair propagate the on-disk mode on purpose (info.Mode().Perm(), with a comment explaining bbolt will not chmod an existing file). They become correct by propagation after this change and are untouched.
  • Envelope-encrypting the token fields with a go-keyring-held key is the right follow-up and a larger, separate PR.
  • ~/.mcpproxy/index.bleve/ and the log files (documented as 0644) are not covered here.

How verified

TDD: five new tests in internal/storage/bbolt_file_mode_test.go (fresh create, migration of an existing 0644 database, owner-bit preservation, backup to a new destination, backup over a pre-existing 0644 destination), each confirmed failing before the fix. They are skipped on Windows, where os.Chmod only toggles the read-only bit and os.Stat reports 0666.

Also verified against a real mcpproxy serve instance on a scratch data dir: a fresh config.db lands -rw-------; loosening it to 0644 and restarting migrates it back with Tightened permissions on ... from 0644 to 0600 at debug level; a third start on an already-0600 database logs nothing.

Cross-reviewed with codex gpt-5.6-sol over three rounds; two rounds returned blocking findings on the backup path (both verified against go.etcd.io/bbolt@v1.5.0/tx.go before fixing), the third returned APPROVE.

🤖 Generated with Claude Code

config.db stores OAuth access/refresh tokens and DCR client secrets as
plain JSON, but was created with mode 0644 and backed up with 0644.

- Open the database 0600.
- Tighten an existing group/world-readable database on open, clearing
  only the 0o077 bits so a stricter owner mode is never widened. This is
  best-effort: failures are logged at debug level and never block startup.
- Stage Backup in an owner-only temp file next to the destination and
  rename it into place. bbolt's Tx.CopyFile opens with O_CREATE|O_TRUNC,
  so its mode argument is ignored for an existing destination and the
  database bytes would land in whatever permissions a stale backup carried.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 22, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: f623533
Status: ✅  Deploy successful!
Preview URL: https://0170deba.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-db-file-mode-0600.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Sep 22, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 58.33333% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/storage/bbolt.go 58.33% 8 Missing and 7 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/db-file-mode-0600

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (22 MB)
  • smart-mcp-proxymcpproxy-goJ2EZP4.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 35814351800 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

tightenFilePermissions logged both its failure branches (stat and
chmod) at Debug, which sits below the project's default zap level
(Info). On a legacy 0644 config.db where chmod cannot succeed
(read-only mount, macOS uchg/schg flag, SELinux/AppArmor denial),
mcpproxy started normally and the DB - which holds OAuth tokens and
DCR client secrets - stayed silently world-readable with no
operator-visible signal that the migration didn't take effect.

Not failing startup is still correct; the log level wasn't. Bump the
two failure branches to Warn and the one-time successful-migration
message to Info so this is visible in default-level logs, per
second-lens review of PR #1342.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Dumbris

Dumbris commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

Second-lens review (zcode/GLM per the current maintainer directive) verified two findings against the code; applied the one that was small and low-risk, left the other as an out-of-diff follow-up:

Applied: tightenFilePermissions in internal/storage/bbolt.go was logging both its failure branches (stat failure, chmod failure) at Debug, below the project's default log level (Info). On a legacy 0644 config.db where chmod can't succeed (read-only mount, macOS uchg/schg, SELinux/AppArmor denial), mcpproxy started normally with the DB — holding OAuth tokens and DCR client secrets — silently staying world-readable, with zero operator-visible signal. Bumped both failure branches to Warn, and the one-time successful-migration message to Info, so this is visible in default-level logs. Startup still never fails on this — only the log level changed. Added two regression tests (TestTightenFilePermissionsLogsStatFailureAtWarn, TestTightenFilePermissionsLogsChmodFailureAtWarn, the latter using chflags uchg to reproduce the macOS-immutable-flag case for real) in internal/storage/bbolt_file_mode_test.go.

Not applied (out of scope for this PR): cmd/mcpproxy/db_cmd.go's compactDatabase carries the source file's original mode forward via os.Chmod(tmpPath, info.Mode().Perm()). Running mcpproxy db compact against a stopped legacy 0644 database (one NewBoltDB has never opened, so tightenFilePermissions never ran) reproduces a 0644 compacted file. It's self-healing on the next normal start and never widens permissions, so it isn't a defect this PR introduced — worth a separate follow-up if full SEC-03 consistency across all DB-touching CLI paths is wanted.

Verified: go build, go test -race ./internal/storage/..., both golangci-lint passes (bare + --build-tags server) clean on the touched files.

This branch has not been deployed

No deployments
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.

2 participants