fix(lib): fail-closed upload containment — default UPLOAD_BASE_DIR to CWD - #366
fix(lib): fail-closed upload containment — default UPLOAD_BASE_DIR to CWD#366SavioBS629 wants to merge 1 commit into
Conversation
… CWD Security retest on PMAA-107 found containment was skipped entirely when MCP_UPLOAD_BASE_DIR was unset (the default), leaving arbitrary-path exfiltration open for allowlisted extensions. Containment now defaults to the process working directory at both layers: config.ts falls back to process.cwd(), and validateUploadPath() itself defaults a missing allowedBaseDir to process.cwd() so no caller can re-open the gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SavioBS629
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 4 inline finding(s). Full report in the PR comment below. Verdict: Failed - see PR comment.
| `Upload rejected: file must be located inside ${baseCanonical}.`, | ||
| ); | ||
| } | ||
| const allowedBaseDir = options.allowedBaseDir ?? process.cwd(); |
There was a problem hiding this comment.
[High] Fail-closed default is vacuous when cwd is /
With cwd /, baseCanonical is "/", baseWithSep is "/", and every absolute path passes startsWith("/") — containment silently allows the entire filesystem. Claude Desktop on macOS launches stdio MCP servers with cwd /, so the fail-closed guarantee fails in one of the most common deployments this fix targets.
Suggestion: after canonicalizing, if the base dir was defaulted (not explicitly configured) and equals the filesystem root (path.parse(baseCanonical).root === baseCanonical), reject with "working directory is the filesystem root; set MCP_UPLOAD_BASE_DIR to enable uploads". Add a cwd=/ test.
Reviewer: stack:code-review
| process.env.MCP_UPLOAD_BASE_DIR && process.env.MCP_UPLOAD_BASE_DIR.length > 0 | ||
| ? process.env.MCP_UPLOAD_BASE_DIR | ||
| : undefined, | ||
| : process.cwd(), |
There was a problem hiding this comment.
[Medium] Undocumented breaking default; MCP_UPLOAD_BASE_DIR documented nowhere
This flips the default from "no containment" to "cwd-only": users uploading from ~/Downloads while the server runs with a project cwd (Cursor/VS Code) will start getting rejections, and MCP_UPLOAD_BASE_DIR appears in no README or feature-flag doc.
Suggestion: document the env var (README env table + FEATURE-FLAGS doc) and call out the behavior change in the next release notes.
Reviewer: stack:code-review
| baseCanonical = fs.realpathSync(path.resolve(allowedBaseDir)); | ||
| } catch { | ||
| throw new Error( | ||
| `Upload rejected: configured MCP_UPLOAD_BASE_DIR does not exist (${allowedBaseDir}).`, |
There was a problem hiding this comment.
[Low] Misleading error when the base dir was defaulted
If the env var is unset and realpathSync fails (e.g. deleted cwd), this blames "configured MCP_UPLOAD_BASE_DIR" even though the user configured nothing.
Suggestion: branch the message on options.allowedBaseDir !== undefined ("configured MCP_UPLOAD_BASE_DIR does not exist" vs "working directory could not be resolved").
Reviewer: stack:code-review
| expect(resolved).toBe(fs.realpathSync(file)); | ||
| }); | ||
|
|
||
| it("defaults containment to the process working directory (fail-closed)", () => { |
There was a problem hiding this comment.
[Low] Test relies on tmpdir being outside vitest's cwd
This passes only because os.tmpdir() happens to be outside the repo cwd; on a runner with TMPDIR inside the workspace it fails spuriously, and with cwd / it passes vacuously.
Suggestion: mock process.cwd() to a sibling temp dir, as the companion test below already does.
Reviewer: stack:code-review
Claude Code PR ReviewPR: #366 • Head: c578585 • Reviewers: stack:code-review SummaryMakes upload path containment fail-closed: Review Table
Findings
Verdict: FAIL — the cwd= |
|
Duplicate PR |
Summary
Security retest on PMAA-107 found upload path containment was skipped entirely when
MCP_UPLOAD_BASE_DIRwas unset (the default), leaving arbitrary-path exfiltration open for allowlisted extensions.Containment now defaults to the process working directory at both layers:
src/config.ts—UPLOAD_BASE_DIRfalls back toprocess.cwd()when the env var is unsetsrc/lib/upload-validator.ts—validateUploadPath()defaults a missingallowedBaseDirtoprocess.cwd(), so no caller can re-open the gapBehavior change
Uploads from outside the working directory now fail unless
MCP_UPLOAD_BASE_DIRis set. The rejection message tells the user how to widen the boundary.Not included
Extension-allowlist narrowing (.json/.txt/.csv) — pushed back on in the ticket, left as an open question.
Testing
npm run buildgreen — lint, format, 275/275 tests (2 new: outside-CWD rejected by default, inside-CWD accepted), tsc.🤖 Generated with Claude Code