Skip to content

fix: secure built-in filesystem MCP root handling#13294

Open
DeJeune wants to merge 2 commits intomainfrom
DeJeune/issue-12447-fix
Open

fix: secure built-in filesystem MCP root handling#13294
DeJeune wants to merge 2 commits intomainfrom
DeJeune/issue-12447-fix

Conversation

@DeJeune
Copy link
Collaborator

@DeJeune DeJeune commented Mar 7, 2026

What this PR does

Before this PR:

  • The built-in filesystem MCP server accepted absolute paths outside the configured workspace root.
  • The in-memory filesystem server only read WORKSPACE_ROOT, so roots configured via args were ignored.
  • The server registered tool handlers asynchronously, which could cause first-load mcp:list-tools failures.
  • Sensitive filesystem tools could be auto-approved by default.
image

After this PR:

  • Filesystem MCP paths are constrained to the configured workspace root after realpath resolution.
  • The built-in filesystem server resolves its root from WORKSPACE_ROOT or the configured args.
  • Tool handlers are registered before async directory initialization starts.
  • Built-in filesystem write, edit, and delete require manual approval by default, including a backfill for existing configs.
image

Fixes #12447

Why we need it and why it was done in this way

The following tradeoffs were made:

  • Existing filesystem MCP configurations now default to safer approval behavior for destructive tools.
  • Path validation resolves symlinks and nearest existing ancestors to block directory escapes for both existing and newly created paths.

The following alternatives were considered:

  • Trusting UI configuration alone without enforcing the root in the filesystem tool layer.
  • Keeping auto-approve defaults unchanged and relying on users to disable risky tools manually.
  • Waiting for async initialization to finish before serving requests instead of registering handlers immediately.

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, and delete by default. Users who relied on automatic approval for these tools need to re-enable auto-approve explicitly.

Special notes for your reviewer

  • This PR does not change Redux state shape or IndexedDB schema.
  • Added regression coverage for root resolution, path escapes, symlink escapes, and filesystem approval defaults.

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.

Release note

action required: The built-in filesystem MCP server now enforces the configured workspace root and requires manual approval for write, edit, and delete operations by default.

Signed-off-by: suyao <sy20010504@gmail.com>
@DeJeune DeJeune requested a review from 0xfullex as a code owner March 7, 2026 18:32
@DeJeune DeJeune requested review from EurFelux and vaayne March 7, 2026 18:45
Copy link
Collaborator

@EurFelux EurFelux left a comment

Choose a reason for hiding this comment

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

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 ✅

  • resolveRealOrNearestExistingPath is a solid approach that handles both existing files (via realpath) and new files (by walking up to the nearest existing ancestor and resolving from there). This correctly blocks symlink-based directory escapes.
  • Splitting initialize() into registerHandlers() (sync) + ensureBaseDir() (async) fixes a real race condition where tools could be called before handlers were registered.
  • resolveFilesystemBaseDir elegantly supports both WORKSPACE_ROOT env and args fallback, 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) 💡

  1. Additional test coverage: relative path traversal (../../), ~ expansion, and no-baseDir fallback
  2. Temp dir location: Consider using os.tmpdir() instead of project-local .context/vitest-temp
  3. Log warning when resolveRealOrNearestExistingPath exhausts all ancestors (edge case)
  4. 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.

Comment on lines +53 to +65
while (true) {
try {
const realCurrentPath = await fs.realpath(currentPath)
const relativeSuffix = path.relative(currentPath, targetPath)
return normalizePath(path.join(realCurrentPath, relativeSuffix))
} catch {
const parentPath = path.dirname(currentPath)
if (parentPath === currentPath) {
return normalizePath(targetPath)
}
currentPath = parentPath
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Potential edge case: root with no existing ancestor

When the workspace root path itself doesn't exist and neither does any of its ancestors (very extreme edge case, e.g. an external drive path before mounting), this loop reaches the filesystem root and falls back to normalizePath(targetPath) without resolving symlinks.

In practice this is unlikely to be exploitable since both resolvedRoot and resolvedPath would both get this fallback treatment, but worth considering adding a log warning when the loop exhausts all ancestors.

if (parentPath === currentPath) {
  logger.warn('Could not resolve any existing ancestor for path', { targetPath })
  return normalizePath(targetPath)
}

nit: Not a blocker.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a warning when ancestor resolution exhausts the path tree before finding any existing entry. This keeps the existing fallback behavior but makes the edge case visible in logs.

Comment on lines 82 to +93
export async function validatePath(requestedPath: string, baseDir?: string): Promise<string> {
const expandedPath = expandHome(requestedPath)
const root = baseDir ?? process.cwd()
const root = expandHome(baseDir ?? process.cwd())
const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(root, expandedPath)

// Handle symlinks by checking their real path
try {
const realPath = await fs.realpath(absolute)
return normalizePath(realPath)
} catch (error) {
// For new files that don't exist yet, verify parent directory
const parentDir = path.dirname(absolute)
try {
const realParentPath = await fs.realpath(parentDir)
normalizePath(realParentPath)
return normalizePath(absolute)
} catch {
return normalizePath(absolute)
}
const resolvedRoot = await resolveRealOrNearestExistingPath(path.resolve(root))
const resolvedPath = await resolveRealOrNearestExistingPath(absolute)

if (!isPathWithinRoot(resolvedPath, resolvedRoot)) {
throw new Error(`Access denied: Path is outside the configured workspace root: ${requestedPath}`)
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

TOCTOU consideration (informational)

There's an inherent TOCTOU (Time-of-check-time-of-use) window between validatePath resolving the real path and the actual file operation. For example, a symlink could be created between the validation and the file read/write.

This is a known limitation of path-based security validation in general and not specific to this PR. Just flagging for awareness — mitigating this fully would require something like O_NOFOLLOW or openat() which is beyond scope here.

Not a blocker — the current approach significantly raises the bar compared to no validation at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added regression coverage for relative traversal (../outside.txt), ~ expansion outside the root, and the process.cwd() fallback path when baseDir is omitted.

Comment on lines +24 to +27
expect(resolveFilesystemBaseDir(['C:/args-root'], {})).toBe('C:/args-root')
expect(resolveFilesystemBaseDir(['C:/args-root'], { WORKSPACE_ROOT: 'C:/env-root' })).toBe('C:/env-root')
expect(resolveFilesystemBaseDir([], {})).toBeUndefined()
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion: Add test cases for relative paths and ~ expansion

The current tests cover the key security scenarios well (root containment, symlink escapes). Consider adding coverage for:

  1. Relative path resolution: validatePath('../../etc/passwd', workspaceRoot) should be rejected
  2. ~ expansion: validatePath('~/sensitive-file', workspaceRoot) should be rejected if home dir is outside root
  3. validatePath with no baseDir: verify fallback to process.cwd() still enforces containment

These would strengthen the regression suite for path traversal attacks.

Comment on lines +245 to +252
const existingFilesystemServer = existingServers.find((server) => server.name === BuiltinMCPServerNames.filesystem)
if (existingFilesystemServer && existingFilesystemServer.disabledAutoApproveTools === undefined) {
dispatch(
updateMCPServer({
...existingFilesystemServer,
disabledAutoApproveTools: [...filesystemManualApprovalTools]
})
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Question: Backfill runs on every app initialization

This backfill code runs on every initializeMCPServers call. After the first run, disabledAutoApproveTools will be set, so the === undefined check will prevent re-applying.

However, what if a user intentionally clears disabledAutoApproveTools (sets it to undefined or deletes the property)? The next initialization would re-apply the defaults, overriding their choice.

A more robust approach might be to use a separate one-time migration flag, e.g. _filesystemApprovalMigrated: true, to track whether the backfill has been applied. But this is a minor concern — the current approach is pragmatic and covers the common case well.

nit: Not a blocker.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I kept the current backfill approach for now. Adding a dedicated migration flag would mean introducing more state into src/renderer/src/store/mcp.ts, which is currently v2-blocked. In this case I think treating undefined as “apply the safer default” is the better tradeoff until a broader migration path is in place.

@EurFelux
Copy link
Collaborator

EurFelux commented Mar 7, 2026

Note

This issue/comment/review was translated by Claude.

I recall that this issue didn't exist earlier. It may have been introduced by #11937 without strict review.


Original Content

印象里早些时候是没有这个问题的。可能是 #11937 未经严格审查引入了该问题。

Signed-off-by: suyao <sy20010504@gmail.com>
@vaayne
Copy link
Collaborator

vaayne commented Mar 8, 2026

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 测试的时候说过这个问题,我是故意没有这个限制的,因为有人配置了目录但是问题更多,不过加上了也好

@vaayne
Copy link
Collaborator

vaayne commented Mar 8, 2026

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

但是我仍然觉得使用绝对路径会更好,看你们吧,和不和我都可以

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]: Built-in MCP service filesystem can access arbitrary directories

3 participants