Skip to content

Confine MCP file resources to the workspace - #629

Closed
logbie wants to merge 3 commits into
mainfrom
agent/restrict-mcp-file-resources
Closed

Confine MCP file resources to the workspace#629
logbie wants to merge 3 commits into
mainfrom
agent/restrict-mcp-file-resources

Conversation

@logbie

@logbie logbie commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • parse MCP file:/// resources as real file URLs and canonicalize both the target and configured workspace
  • reject traversal, absolute-path, and symlink escapes outside the workspace
  • expose only regular .wfl source files and cap each resource/config/symbol read at 4 MiB
  • apply the same containment check to a symlinked .wflcfg
  • stop echoing MCP request and response bodies to diagnostic stderr
  • add valid, outside-workspace, symlink-escape, oversized-file, and config-symlink regressions

Security impact

A caller of the local MCP resources/read method could previously select paths outside the workspace and make the server buffer them without a source-level limit. Malicious workspace symlinks could also redirect workspace://config to an external file. The server now returns only bounded, canonical workspace-owned source/config content and no longer copies source payloads into logs.

Validation

  • changed Rust source passes a tree-sitter Rust syntax scan
  • git diff --check passes
  • uploaded Git blob and tree hashes match the inspected local commit
  • native Cargo/rustfmt are unavailable locally; repository CI is the source-of-truth validation and passed on the final head
  • all GitHub CI, config lint, and review checks passed on the final head

Part of the Rust-source security audit tracked in #610.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@logbie, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d772254a-a8ee-4c6a-9375-a9c9fd820164

📥 Commits

Reviewing files that changed from the base of the PR and between 017e698 and f87dfce.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • wfl-lsp/src/mcp_server.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/restrict-mcp-file-resources

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@logbie
logbie marked this pull request as ready for review July 16, 2026 18:13
Copilot AI review requested due to automatic review settings July 16, 2026 18:13

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread wfl-lsp/src/mcp_server.rs

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.

🔍 Advertised file:/// URIs use four leading slashes on Unix; round-trip through Url::to_file_path is untested

handle_workspace_files advertises resource URIs as format!("file:///{}", path.replace("\\", "/")) (wfl-lsp/src/mcp_server.rs:916). On Unix, entry.path() is an absolute path beginning with / (e.g. /ws/program.wfl), so the emitted URI becomes file:////ws/program.wfl (four slashes), which is not the canonical serialization url produces (Url::from_file_path yields three slashes). The new reader handle_file_resource (wfl-lsp/src/mcp_server.rs:995) now parses the URI via Url::parse(...).to_file_path() instead of the old strip_prefix("file:///"). The old strip-prefix approach round-tripped the four-slash form fine (yielding /ws/program.wfl). Whether Url::to_file_path() returns a usable path for the non-canonical four-slash form (empty-host file URL with a leading empty path segment) is not exercised by any test — all new tests build URIs with Url::from_file_path (the canonical three-slash form). If to_file_path() fails or returns a path that then fails canonicalize/starts_with, clients that read a URI verbatim from workspace://files could get 'Invalid local file resource URI' or 'does not exist' errors even for legitimate in-workspace files. Worth confirming the round-trip against the pinned url crate version, or normalizing the advertised URI via Url::from_file_path.

(Refers to line 916)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2332593361

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wfl-lsp/src/mcp_server.rs
data: None,
}),
},
let requested_path = match Url::parse(uri).ok().and_then(|url| url.to_file_path().ok()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Encode listed file URIs before parsing them

When a workspace file name contains URL-reserved characters such as # or ?, workspace://files still emits the raw path (for example file:////.../foo#bar.wfl). With the new Url::parse(...).to_file_path() here, those characters are interpreted as fragment/query delimiters before canonicalization, so a client that lists resources and then reads the returned URI gets File resource does not exist even though the .wfl file is inside the workspace. Please emit/read proper file URLs, e.g. by using Url::from_file_path, so listed resources round-trip.

Useful? React with 👍 / 👎.

Copilot AI 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.

Pull request overview

This PR hardens the WFL MCP server’s resources/read handling to ensure file:/// resources and .wflcfg reads are confined to the configured workspace, preventing traversal/symlink escapes and limiting read sizes to reduce data exposure and memory risk.

Changes:

  • Canonicalizes workspace root + requested file:/// target, rejects out-of-workspace/symlink escapes, and restricts readable resources to .wfl files.
  • Adds a bounded text reader (4 MiB cap) and applies it to file resources, symbol scanning, diagnostics scanning, and workspace config reads.
  • Stops echoing full MCP request/response payloads to stderr and adds regression tests for workspace containment and size limits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
wfl-lsp/src/mcp_server.rs Adds canonicalization/containment enforcement, 4 MiB bounded reads, quieter logging, and new regression tests for MCP resource/config reads.
CHANGELOG.md Documents the new MCP workspace containment and logging changes under Security.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread wfl-lsp/src/mcp_server.rs
Comment on lines +995 to +998
let requested_path = match Url::parse(uri).ok().and_then(|url| url.to_file_path().ok()) {
Some(path) => path,
None => return Self::file_resource_error(id, "Invalid local file resource URI"),
};
Comment thread wfl-lsp/src/mcp_server.rs
Comment on lines +941 to +951
fn file_resource_error(id: Option<Value>, message: impl Into<String>) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError {
code: -32602,
message: message.into(),
data: None,
}),
}
Copilot AI review requested due to automatic review settings July 16, 2026 18:45

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread wfl-lsp/src/mcp_server.rs
Comment on lines +995 to +999
let requested_path = match Url::parse(uri).ok().and_then(|url| url.to_file_path().ok()) {
Some(path) => path,
None => return Self::file_resource_error(id, "Invalid local file resource URI"),
};
let requested_path = match requested_path.canonicalize() {
Comment thread wfl-lsp/src/mcp_server.rs
Comment on lines +955 to +960
let metadata = path
.metadata()
.map_err(|_| "File resource is not readable".to_string())?;
if !metadata.is_file() {
return Err("File resource is not a regular file".to_string());
}
Comment thread wfl-lsp/src/mcp_server.rs
Comment on lines +969 to +981
fs::File::open(path)
.and_then(|file| {
file.take(MAX_MCP_RESOURCE_BYTES + 1)
.read_to_end(&mut bytes)
})
.map_err(|_| "Failed to read file resource".to_string())?;
if bytes.len() as u64 > MAX_MCP_RESOURCE_BYTES {
return Err(format!(
"File resource exceeds the {} byte limit",
MAX_MCP_RESOURCE_BYTES
));
}
String::from_utf8(bytes).map_err(|_| "File resource is not valid UTF-8".to_string())

logbie commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #632, which preserves this security fix in the consolidated Rust-source hardening PR. The combined head is mergeable and all required CI checks are green.

@logbie logbie closed this Jul 17, 2026
@logbie
logbie deleted the agent/restrict-mcp-file-resources branch August 14, 2026 04:29
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