fix: secure built-in filesystem MCP root handling#13294
Conversation
Signed-off-by: suyao <[email protected]>
EurFelux
left a comment
There was a problem hiding this comment.
Review Summary
This is a well-structured security fix that addresses a real vulnerability — the built-in filesystem MCP server previously had no path containment enforcement, allowing absolute paths outside the configured workspace root to be accessed freely.
What's Good ✅
resolveRealOrNearestExistingPathis a solid approach that handles both existing files (viarealpath) and new files (by walking up to the nearest existing ancestor and resolving from there). This correctly blocks symlink-based directory escapes.- Splitting
initialize()intoregisterHandlers()(sync) +ensureBaseDir()(async) fixes a real race condition where tools could be called before handlers were registered. resolveFilesystemBaseDirelegantly supports bothWORKSPACE_ROOTenv andargsfallback, making the configuration more flexible.- Backfill for existing configs is a pragmatic approach to ensure users upgrading get safer defaults without breaking their setup.
- Tests cover the key attack vectors: root containment, symlink escapes.
- Tool descriptions updated to reflect the new constraints — good attention to detail.
Suggestions (Non-blocking) 💡
- Additional test coverage: relative path traversal (
../../),~expansion, and no-baseDir fallback - Temp dir location: Consider using
os.tmpdir()instead of project-local.context/vitest-temp - Log warning when
resolveRealOrNearestExistingPathexhausts all ancestors (edge case) - TOCTOU awareness: Inherent limitation noted for future hardening consideration
Regarding v2 Blocked File
This PR modifies src/renderer/src/store/mcp.ts which has a v2 deprecation header. However, this is a security fix that qualifies as a critical bug fix under the contribution guidelines. The changes are minimal and don't alter the Redux state shape.
Overall: solid security improvement. The inline comments are suggestions for hardening, not blockers.
Signed-off-by: suyao <[email protected]>
|
Note This issue/comment/review was translated by Claude. @kangfenmao mentioned this issue during testing before. I intentionally didn't have this restriction because some people configured directories but had more issues, but adding it is also good. Original Content之前 @kangfenmao 测试的时候说过这个问题,我是故意没有这个限制的,因为有人配置了目录但是问题更多,不过加上了也好 |
|
Note This issue/comment/review was translated by Claude. I still think using absolute paths would be better, it's up to you guys, I'm fine either way Original Content但是我仍然觉得使用绝对路径会更好,看你们吧,和不和我都可以 |
EurFelux
left a comment
There was a problem hiding this comment.
LGTM! 🛡️ Solid security hardening for the built-in filesystem MCP server.
What's well done:
resolveRealOrNearestExistingPathcorrectly handles both existing files (viarealpath) and new files (nearest ancestor resolution), blocking symlink-based directory escapes- Splitting
initialize()→ syncregisterHandlers()+ asyncensureBaseDir()fixes the tool registration race condition isPathWithinRootwithpath.relativeis clean and handles edge cases (Windows case-insensitivity, cross-drive paths)- Good test coverage: root containment, symlinks, relative traversal,
~expansion,cwd()fallback - Backfill approach for
disabledAutoApproveToolsis pragmatic given v2 constraints
All previous review suggestions have been addressed. Approving.
EurFelux
left a comment
There was a problem hiding this comment.
LGTM! Solid security hardening for the built-in filesystem MCP server.
What's well done:
resolveRealOrNearestExistingPathcorrectly handles both existing files (viarealpath) and new files (nearest ancestor resolution), blocking symlink-based directory escapes- Splitting
initialize()→ syncregisterHandlers()+ asyncensureBaseDir()fixes the tool registration race condition isPathWithinRootwithpath.relativeis clean and handles edge cases (Windows case-insensitivity, cross-drive paths)- Good test coverage: root containment, symlinks, relative traversal,
~expansion,cwd()fallback - Backfill approach for
disabledAutoApproveToolsis pragmatic given v2 constraints
All previous review suggestions have been addressed. Approving.
The backfill in initializeMCPServers was dead code — the function is exported but never called at runtime. Existing filesystem server entries in persisted Redux state would never receive disabledAutoApproveTools. Move the backfill to redux-persist migration version 200, which actually runs against persisted state on app startup. Remove the dead code from initializeMCPServers and update tests to verify the migration path. Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: suyao <[email protected]>
| return envWorkspaceRoot | ||
| } | ||
|
|
||
| return args.find((arg) => typeof arg === 'string' && arg.trim().length > 0)?.trim() |
There was a problem hiding this comment.
Note: This change makes the built-in filesystem server effectively single-root by taking only the first non-empty arg, while the settings UI and copy still imply that multiple filesystem directories can be configured. I do not think this needs to block the security fix, but it would be good to either align the UI/copy with a single-root model or track multi-root support explicitly in a follow-up issue.
| - Pattern syntax: * (any chars), ** (any path), {a,b} (alternatives), ? (single char) | ||
| - Results are limited to 100 files | ||
| - The path parameter must be an absolute path if specified | ||
| - The path parameter must resolve within the configured workspace root if specified |
There was a problem hiding this comment.
Security: This updated description now promises that the path stays within the configured workspace root, but handleGlobTool still runs ripgrep with --follow and never re-validates each returned match against the root. A symlinked directory inside the workspace can therefore still make glob enumerate files outside the configured root. Please either drop --follow or validate each ripgrep result with the same realpath and root check before returning it.
|
Follow-up: to clarify my earlier COMMENT review, I consider the remaining The single-root vs multi-directory configuration note is non-blocking and can be addressed either in this PR or in a follow-up issue. |
The glob tool uses ripgrep --follow which traverses symlinks, and the returned file paths were not re-validated against the workspace root. Similarly, the grep tool (both ripgrep and fallback paths) and the ls tool's recursive mode could enumerate content outside the root via symlinked directories. Re-validate each result path against the workspace root in glob, grep, and ls to filter out entries that resolve outside the configured root after symlink resolution. Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: suyao <[email protected]>
|
Fixed in 61d68c6. All three enumeration tools now re-validate each result path against the workspace root after resolution:
Added two regression tests:
|
Signed-off-by: suyao <[email protected]> # Conflicts: # src/renderer/src/store/migrate.ts
The glob symlink test called handleGlobTool which invokes runRipgrep, but runRipgrep depends on @anthropic-ai/claude-agent-sdk/package.json which is not exported in CI. Mock runRipgrep to return both the legit and escaped file paths, so the test validates the validatePath filtering without requiring the ripgrep binary. Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: suyao <[email protected]>
zhibisora
left a comment
There was a problem hiding this comment.
Thanks for addressing the symlink-traversal gap and tightening the filesystem MCP sandboxing. The remaining blocking issue I called out in glob is fixed on the latest PR head, the related coverage is in place, and CI is green. I still see the single-root versus multi-directory configuration mismatch as a non-blocking follow-up, but this looks good to merge from my side.
Signed-off-by: suyao <[email protected]> # Conflicts: # src/renderer/src/store/migrate.ts
### What this PR does Before this PR: - Version is 1.7.24 After this PR: - Version is 1.7.25 - Release notes updated in `electron-builder.yml` ### Why we need it and why it was done in this way Standard release workflow: collect commits since v1.7.24, generate bilingual release notes, bump version. The following tradeoffs were made: N/A The following alternatives were considered: N/A ### Breaking changes **Action Required**: The built-in filesystem MCP server now requires manual approval for write/edit/delete operations by default. ### Special notes for your reviewer **Included commits since v1.7.24:** ✨ New Features: - feat(websearch): add Querit search provider (#13050) - feat(minapp,provider): add MiniMax Agent, IMA mini apps and MiniMax Global, Z.ai providers (#13099) - feat(provider): add agent support filter for provider list (#11932) - feat(agent): add custom environment variables to agent configuration (#13357) - feat: add GPT-5.4 support (#13293) - feat(gemini): add thought signature persistence (#13100) 🐛 Bug Fixes: - fix: secure built-in filesystem MCP root handling (#13294) - fix: check underlying tool permissions for hub invoke/exec (#13282) - fix: remove approval countdown timers and add system notifications (#13281) - fix: agent tool status not stopping on abort (#13111) - fix: resolve spawn ENOENT on Windows for Code Tools (#13405) - fix: Use default assistant for topic auto-renaming (#13387) - fix(backup): defer auto backup during streaming response (#13307) - fix(ui): fix Move To submenu overflow (#13399) - fix: render xml fenced svg blocks as svg previews (#13431) - fix: correct Gemini reasoning params (#13388) - fix: improve Qwen 3.5 reasoning model detection (#13235) - fix: upgrade xAI web search to Responses API (#12812) - fix(api-server): relax chat completion validation (#13279) - fix: install or uninstall button state issues (#13114) - fix: improve update dialog behavior (#13363) - fix: auto-convert reasoning_effort to reasoningEffort (#12831) ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and Keep it simple - [x] Refactor: You have left the code cleaner than you found it (Boy Scout Rule) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A user-guide update was considered and is present (link) or not required. - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note See release notes in electron-builder.yml ``` --------- Signed-off-by: kangfenmao <[email protected]>
What this PR does
Before this PR:
WORKSPACE_ROOT, so roots configured viaargswere ignored.mcp:list-toolsfailures.After this PR:
WORKSPACE_ROOTor the configuredargs.write,edit, anddeleterequire manual approval by default, including a backfill for existing configs.Fixes #12447
Why we need it and why it was done in this way
The following tradeoffs were made:
The following alternatives were considered:
Links to places where the discussion took place: #12447, #13280
Breaking changes
The built-in filesystem MCP server now requires manual approval for
write,edit, anddeleteby default. Users who relied on automatic approval for these tools need to re-enable auto-approve explicitly.Special notes for your reviewer
Checklist
This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.
/gh-pr-review,gh pr diff, or GitHub UI) before requesting review from othersRelease note