Skip to content

fix(sandbox): document stdio data-dir/socket exposure, doctor warning, writable /dev/null (#1704) - #1738

Merged
github-actions[bot] merged 1 commit into
mainfrom
claude/cool-dhawan-0b193a
Oct 22, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
claude/cool-dhawan-0b193a

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Closes #1704

Delivers what was promised on the issue: honest docs, a doctor warning, and a check of the /dev/null side observation (which turned out to be a real bug, fixed here).

Docs

none and sandbox isolation do not hide the data directory or the Unix socket from a stdio child: the child runs as the proxy's user, can read mcp_config.json (api_key) and config.db (OAuth tokens), and while enable_socket is on it can mint agent tokens over <data-dir>/mcpproxy.sock without a key. Landlock confines writes only (it does block /proc/<pid>/environ).

  • features/sandbox-isolation: new What the sandbox does not hide section with @james-crowley's verification table and wording, plus two new honest-limits bullets (data dir / socket; snap-packaged runtimes).
  • features/docker-isolation and the unpublished root docs/docker-isolation.md: sandbox and none rows of the mode table say so; a new Honest limitations bullet links the table.
  • socket-communication: trust-boundary bullet under API Key Authentication.
  • getting-started/installation: headless recommendation (enable_socket: false + docker isolation) in the LAN security checklist; the headless example config now sets enable_socket: false.

Website build is green and the new anchor resolves from all three linking pages (checked in website/build).

Doctor

config.StdioSocketExposureFinding joins the existing DoctorFindings → runtime_warnings path. It names every enabled stdio server whose resolved isolation mode is none or sandbox while enable_socket is true, and states the fix:

1 enabled stdio server(s) run under isolation none or sandbox while enable_socket is true ("inherits-none"): a stdio server runs as the proxy's user, can read the data directory (mcp_config.json holds api_key, config.db holds OAuth tokens) and can mint agent tokens over the admin socket in that directory without a key; the sandbox mode confines writes only. Set enable_socket: false on headless installs and use docker isolation (docker_isolation.mode: "docker", or per-server isolation.mode) for servers you do not trust. See https://docs.mcpproxy.app/features/sandbox-isolation/

Skipped: servers resolved to docker, servers whose command is docker itself, HTTP/SSE servers, disabled servers. A per-server enabled: false opt-out under a global docker mode is listed. Tests in internal/config/doctor_findings_test.go. doctor still exits 0; the finding counts toward total_issues like every other runtime warning.

/dev/null under sandbox — confirmed and fixed

Reproduced on a real kernel (kubic, Ubuntu 24.04, kernel 6.8, Landlock ABI 4) with the production spec: sh -c '… 2>/dev/null' fails with cannot create /dev/null: Permission denied. The read-only / rule covers READ_FILE but not WRITE_FILE on the device.

Two changes:

  1. buildSandboxSpec grants /dev/null read-write (the single device, not /dev).
  2. addPathRule masks directory-only rights (READ_DIR, MAKE_*, REMOVE_*, REFER) when the target is not a directory. Without this Landlock rejects the rule with EINVAL, and because the wrapper is fail-closed the whole confinement aborted (exit 3) — so a naive /dev/null grant would have broken every sandboxed server.

What the probe showed with the real mcpproxy __sandbox_exec wrapper on kubic (RO /, RW tmp + caches):

without the grant with the grant
sh -c 'echo x 2>/dev/null' Permission denied ok
Node spawnSync(…, {stdio: "ignore"}) EACCES ok
Python subprocess.run(…, stdout=DEVNULL) PermissionError ok
npx --version; npx -y @modelcontextprotocol/server-everything stdio → initialize ok ok
uvx --version; uvx mcp-server-time → initialize (non-snap uv) ok ok
snap-packaged uvx snap-confine is packaged without necessary permissions same
echo x >/dev/stdout ok (pipe, not a filesystem object) ok

So npx/uvx servers launched fine before; what broke was any server that routes a child's stdio to /dev/null, and shell launch wrappers using 2>/dev/null. The snap finding is independent of /dev/null: Landlock requires no_new_privs, which stops snap-confine from gaining the privileges it needs. Documented as a limitation; the fix is a non-snap runtime install.

Tests: TestBuildSandboxSpec_DevNullWritable (any OS), TestSandboxWrapper_DefaultSpecAllowsDevNull (internal/upstream/core, linux), TestAddPathRule_FileTarget (internal/sandbox, linux). The two linux-tagged tests were watched red then green on kubic via cross-compiled test binaries; macOS cannot run them (Landlock is Linux-only).

Verification

  • golangci-lint with .github/.golangci.yml: bare, --build-tags server, and GOOS=linux for the _linux.go files — no findings in changed files (the local v2 reports two pre-existing reflect.Ptr govet items in untouched test files, identical on main).
  • go test -race ./internal/config/... ./internal/sandbox/... ./internal/management/..., -tags server config tests, cmd/mcpproxy doctor tests, internal/server + internal/httpapi doctor/bundle tests under the CI skip regex: green.
  • ./scripts/test-api-e2e.sh on a private port (LISTEN_PORT=18790, with mcpproxy-server built): 70 run, 0 failed.
  • website && npm run build: green.

No new dependencies.

…ning, make /dev/null writable

Under isolation none or sandbox a stdio child runs as the proxy's user: it
can read the data directory (mcp_config.json with api_key, config.db with
OAuth tokens) and, while enable_socket is on, mint agent tokens over the
admin socket without a key. Landlock confines writes only, so hiding the
files would not close the gap (#1704, verified by the reporter).

- docs: state the limit in the sandbox page (new "What the sandbox does not
  hide" section with the reporter's verification table), the isolation mode
  tables and honest limitations, the socket page, and the headless install
  checklist (enable_socket: false + docker isolation).
- doctor: config.StdioSocketExposureFinding names every enabled stdio
  server resolving to none/sandbox while enable_socket is true, with the
  fix; wired through the existing DoctorFindings runtime_warnings path.
- sandbox: the default spec never granted WRITE_FILE on /dev/null, so a
  confined `sh -c '... 2>/dev/null'`, Node stdio:"ignore" and Python
  subprocess.DEVNULL failed with EACCES (confirmed on kernel 6.8, Landlock
  ABI 4). buildSandboxSpec now grants /dev/null read-write and addPathRule
  masks directory-only rights for non-directory targets, which Landlock
  otherwise rejects with EINVAL (and the fail-closed wrapper then aborted
  the whole confinement).
- docs: snap-packaged runtimes cannot run under the sandbox (snap-confine
  needs privileges no_new_privs forbids).

Closes #1704
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9f14004
Status: ✅  Deploy successful!
Preview URL: https://29db65a4.mcpproxy-docs.pages.dev
Branch Preview URL: https://claude-cool-dhawan-0b193a.mcpproxy-docs.pages.dev

View logs

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved (Model B): Paperclip review verdicts = ACCEPT and qa-gate green at this head SHA. Arming auto-merge; GitHub merges when all required checks pass.

@github-actions
github-actions Bot enabled auto-merge (squash) October 22, 2026 15:20
@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: claude/cool-dhawan-0b193a

Available Artifacts

  • archive-darwin-amd64 (32 MB)
  • archive-darwin-arm64 (29 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (32 MB)
  • archive-windows-arm64 (28 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (28 MB)
  • installer-dmg-darwin-arm64 (25 MB)
  • smart-mcp-proxymcpproxy-goE1FY46.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 39795118329 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

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 92.85714% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/sandbox/sandbox_linux.go 60.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
github-actions Bot merged commit 27eb24a into main Oct 22, 2026
51 checks passed
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.

[Bug]: stdio upstreams can read the proxy's data directory (api_key, OAuth token DB); sandbox mode does not prevent it

2 participants