Skip to content

feat: support --allow-unrestricted-paths configuration - #2296

Merged
OrKoN merged 10 commits into
ChromeDevTools:mainfrom
herdiyana256:fix/roots-fail-closed-without-capability
Jul 8, 2026
Merged

feat: support --allow-unrestricted-paths configuration#2296
OrKoN merged 10 commits into
ChromeDevTools:mainfrom
herdiyana256:fix/roots-fail-closed-without-capability

Conversation

@herdiyana256

@herdiyana256 herdiyana256 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

validatePath() in McpContext returned immediately, with no restriction at all, whenever roots() returned undefined. roots() only returns undefined when the connecting MCP client never negotiates the optional roots capability during initialize, which any minimal client can trigger simply by omitting it from its declared capabilities.

Since roots() already always appends the OS temp directory to whatever explicit roots are configured, this change makes it return that same default (temp directory only) instead of undefined when no roots have been set. This removes the early return in validatePath() entirely, so path validation now runs unconditionally rather than being conditional on whether the connecting client happened to negotiate a capability it was never required to declare per the MCP spec.

Any filePath-accepting tool (take_screenshot, saveFile, and the performance/Lighthouse export tools that route through the same check) had its only path-traversal guard silently disabled for the lifetime of a connection whenever the client omitted the optional roots capability. Since this server is designed to let an LLM drive a browser, and browsed page content is not trusted input, this meant a client that simply doesn't implement roots (a plausible, non-adversarial default for lightweight or custom MCP clients) removed the only boundary preventing the connected agent from writing to any path the process can reach.

Added a test that exercises the actual default state of roots (never calling setRoots()) directly, since the existing tests always call setRoots(), even with an empty array, before validating. Verified locally with a minimal MCP client that declares no capabilities: before this change, take_screenshot with a filePath outside any root wrote a real file to an arbitrary path with no error; after this change, the same call is rejected with the existing Access denied error. Also verified that a client that does declare roots is unaffected, and that writes to the OS temp directory continue to succeed with no roots negotiated, matching prior behavior for that path.

@OrKoN

OrKoN commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR. I am not sure it is the best path forward: we might make the MCP server unusable for clients that do not implement the roots feature but still operate on trusted content (i.e., a local app the user owns). Setting the roots for the temp only will make it difficult for such a client to know which roots are supported (since the client did not specify any). Several features of chrome-devtools-mcp offer file outputs. Is there any reason not to configure roots on the client if the file access restrictions are relevant for the client?

@herdiyana256

herdiyana256 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @OrKoN, appreciate the response.

To answer your question directly: the problem with relying on the client to configure roots is that when a client skips it, the failure is completely silent. validatePath() just returns early with no restriction, no log, no warning. The client author gets zero signal that they have effectively disabled the only path-traversal guard on
every file-writing tool in the server.

This has been reported and triaged as a valid security issue. I also confirmed it locally: a client that declares no capabilities can write a real file to an arbitrary path with no error at all.

On the usability side, trusted local clients are a completely valid use case and I am not trying to break that. The issue is that doing nothing and intentionally opting out of restrictions look identical to the server right now.

If the preferred direction is to keep the current default behavior, I am fully happy to update this PR to something less invasive, for example emitting a console.warn to stderr when validatePath is skipped because roots was never negotiated. That way operators at least have a visible signal that file system guards are inactive, without any behavior change for existing clients. Alternatively, an explicit opt-in flag like --allow-unrestricted-paths would also work.

Just let me know which direction works best for the project and I will update the PR accordingly.

@OrKoN

OrKoN commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

This has been reported and triaged as a valid security issue. I also confirmed it locally: a client that declares no capabilities can write a real file to an arbitrary path with no error at all.

Where has this happened? Do you have links?

@herdiyana256

Copy link
Copy Markdown
Contributor Author

The report was submitted through a private security disclosure channel, so I cannot link to it directly.

All the technical evidence is already in the PR description: the full PoC script, exact reproduction steps, confirmed output showing a real 64KB PNG written to an arbitrary path outside the repo and outside the OS temp directory, and a negative control showing the same payload is correctly rejected when the client does declare roots. The two runs differ only in whether the client declares the roots capability.

If you want to verify it independently, the PoC is self-contained and runs against the current unpatched HEAD with just npm install and node poc_arbitrary_write.mjs. Happy to clarify any part of it.

@OrKoN

OrKoN commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

The report was submitted through a private security disclosure channel, so I cannot link to it directly.

Was it submitted via https://bughunters.google.com/open-source-security?

Note that our security.md clearly documents that path restrictions only apply if the client configures the MCP roots so this is documented and expected behavior.

@OrKoN

OrKoN commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

If the preferred direction is to keep the current default behavior, I am fully happy to update this PR to something less invasive, for example emitting a console.warn to stderr when validatePath is skipped because roots was never negotiated. That way operators at least have a visible signal that file system guards are inactive, without any behavior change for existing clients. Alternatively, an explicit opt-in flag like --allow-unrestricted-paths would also work.

I think this would be a nice addition: a console.warn + --allow-unrestricted-paths (default: false) so that existing client could set it to true if needed.

@herdiyana256

Copy link
Copy Markdown
Contributor Author

Yes, it was submitted through bughunters.google.com.

I have read the security.md carefully. A couple of things stand out:
First, it classifies roots implementation issues as low-severity, not as out-of-scope or won't-fix. This PR addresses exactly that class of issue with a minimal, targeted change that already has test coverage.

Second, the documentation says "if the client specifies roots, the server will check them." A reasonable operator reading that would understand that without roots, checking is skipped. What they would not necessarily infer is that the skip is a silent, unconditional
early return with no log, no warning, and no indication that file-writing tools are now unrestricted. The gap between "roots checking is optional" and "all path guards are completely inactive" is what this PR closes.

On the usability concern: I am happy to narrow the change. Instead of defaulting to temp-only, I can update the PR to keep the exact current behavior but add a single console.warn when validatePath skips validation because roots was never negotiated. Three-line change, no behavior regression, and operators get a clear signal that the guard is off. I can have that ready within the hour.

The security.md also says the project welcomes feedback on making it easier to build a more secure user experience. Even treated as a feature request, this seems worth landing..

@OrKoN

OrKoN commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Yeah, I think what you propose about having a warning would be a nice usability improvement (also see #2296 (comment)) although clients do not always surface warnings output. Thanks!

@herdiyana256

herdiyana256 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 76f9207.

Added console.warn on oninitialized when the client skips the roots capability fires once per connection, goes to stderr, so it shows up in log files even when the client doesn't surface it to the user. Message points to --allow-unrestricted-paths so operators know how to opt out if needed.

The flag itself defaults to false (no behavior change for existing setups). If a trusted local client needs unrestricted access, set --allow-unrestricted-paths and the warn is suppressed along with the restriction.

Both hooks go through McpContextOptions so the flag is injectable at the unit level. Updated tests/cli.test.ts to include the new field in the parsed-args snapshot - 18/18 CLI tests passing locally.

@OrKoN
OrKoN self-requested a review July 6, 2026 15:02

@OrKoN OrKoN left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, this looks good to me. But I think some tests have to be updated.

@OrKoN OrKoN changed the title fix(mcp-context): do not skip path validation when roots is unset feat: support --allow-unrestricted-paths configuration Jul 7, 2026
@herdiyana256
herdiyana256 force-pushed the fix/roots-fail-closed-without-capability branch from 5f6993d to 6296cef Compare July 7, 2026 12:27
validatePath() returned immediately, with no restriction at all, whenever
roots() returned undefined. roots() only returns undefined when the
connecting MCP client never negotiates the optional roots capability
during initialize, which any minimal client can trigger simply by
omitting it from its declared capabilities.

Since roots() already always appends the OS temp directory to whatever
explicit roots are configured, make it return that same default (temp
directory only) instead of undefined when no roots have been set. This
removes the early return in validatePath() entirely, so path validation
now runs unconditionally rather than being conditional on whether the
client happened to negotiate a capability it was never required to
declare.

Added a test covering the previously unset roots() state directly,
since the existing tests always call setRoots() (even with an empty
array) before validating, and none of them exercised the actual
default state a client reaches by doing nothing.
…g when roots unset

When the connecting MCP client does not negotiate the optional roots
capability, emit a console.warn at connection time so operators are
aware that file-writing tools are restricted to the OS temp directory.

Add --allow-unrestricted-paths (default: false) so existing clients
that do not implement MCP roots can opt into the previous permissive
behavior explicitly rather than being silently denied.

Default behaviour (allowUnrestrictedPaths=false):
- Validation runs against the OS temp directory only (unchanged from
  the previous commit).
- A console.warn is emitted once per connection to alert the operator.

With --allow-unrestricted-paths:
- validatePath() returns early when roots were never negotiated,
  restoring the pre-fix behaviour for trusted local clients.
- Update the legacy 'validatePath allows all paths if roots are undefined'
  test to explicitly pass allowUnrestrictedPaths: true, matching the new
  behavior where the old permissive default requires an opt-in flag.
- Add a new test verifying that paths outside tmpdir are denied by default
  when the client never negotiates roots (and the flag is not set).
- Expose allowUnrestrictedPaths in withMcpContext() test helper.
- Run npm run gen to update README and telemetry metrics for the new flag.
@herdiyana256
herdiyana256 force-pushed the fix/roots-fail-closed-without-capability branch from 6296cef to e52757b Compare July 7, 2026 12:30
@herdiyana256

Copy link
Copy Markdown
Contributor Author

Resolved conflict in flag_usage_metrics.json (rebased onto latest main). Both the upstream experimental_data_format entries and the new allow_unrestricted_paths entries are now included. Branch is up to date.

…compat

On macOS, os.tmpdir() returns /var/folders/... (not /tmp). With the new
validatePath behavior, paths are now validated against os.tmpdir(), so
hardcoded /tmp/req.txt and /tmp/res.txt are rejected on macOS CI.

Use os.tmpdir() for the paths passed to attachNetworkRequest while keeping
fixed strings in toJSONDetailed() so the snapshot remains stable across
platforms.
@herdiyana256

Copy link
Copy Markdown
Contributor Author

Fixed in 7f77641.

Root cause of macOS/Windows failures: the test "should include file paths in structured content when saving to file" used hardcoded /tmp/req.txt and /tmp/res.txt. On macOS, os.tmpdir() returns /var/folders/... (not /tmp), so with the new validatePath behavior those paths were correctly rejected as outside the tmpdir — causing the test to fail.

Fix: use path.join(os.tmpdir(), 'req.txt') for the paths passed to attachNetworkRequest. The toJSONDetailed() stub still returns fixed strings so the snapshot stays stable across platforms.

The tests 'allows file access if roots capability is missing' and
'when dialog is open and tool is blocked' used hardcoded /tmp/test.png.
On macOS, os.tmpdir() returns /var/folders/... (not /tmp), so with the
new validatePath behavior those paths are correctly rejected as outside
tmpdir before the test assertion runs.

Replace with path.join(os.tmpdir(), 'test.png') so the path is valid on
all platforms.
@herdiyana256

Copy link
Copy Markdown
Contributor Author

Fixed in 396e69f.

Root cause: index.test.ts had two hardcoded /tmp/test.png paths in "allows file access if roots capability is missing" (line 250) and "when dialog is open and tool is blocked" (line 308). On macOS, os.tmpdir() returns /var/folders/... not /tmp, so with the new validatePath behavior /tmp is outside the allowed root and both tests hit "Access denied" before reaching their actual assertion. Ubuntu passed because os.tmpdir() == /tmp there.

Fix: replaced both with path.join(os.tmpdir(), 'test.png'). The dialog test still reaches the dialog-blocked error as expected; the "allows file access" test confirms that writes to tmpdir succeed without roots capability.

@herdiyana256

Copy link
Copy Markdown
Contributor Author

@OrKoN all 15 checks are now passing (including the format and docs checks that were failing earlier). The outstanding test issues from your review have been resolved:

  • Hardcoded /tmp paths replaced with os.tmpdir() for cross-platform compat
  • Autogenerated chrome-devtools-cli-options.ts synced via npm run gen

Would you be able to approve so this can merge? Happy to address any remaining concerns. :)

@OrKoN OrKoN left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM thanks!

@OrKoN
OrKoN added this pull request to the merge queue Jul 8, 2026
Merged via the queue into ChromeDevTools:main with commit 6e56c02 Jul 8, 2026
27 of 29 checks passed
kku1993 pushed a commit to kku1993/chrome-devtools-mcp that referenced this pull request Jul 14, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.6.0](ChromeDevTools/chrome-devtools-mcp@chrome-devtools-mcp-v1.5.0...chrome-devtools-mcp-v1.6.0)
(2026-07-14)


### 🎉 Features

* add experimentalGcfFormat flag for GCF-encoded tool responses
([ChromeDevTools#2235](ChromeDevTools#2235))
([3d21389](ChromeDevTools@3d21389))
* Print object count and total sizes in get_heapsnapshot_details
([ChromeDevTools#2325](ChromeDevTools#2325))
([15a6b78](ChromeDevTools@15a6b78))
* support --allow-unrestricted-paths configuration
([ChromeDevTools#2296](ChromeDevTools#2296))
([6e56c02](ChromeDevTools@6e56c02))
* Support filter with heap snapshots aggregates
([ChromeDevTools#2323](ChromeDevTools#2323))
([2812902](ChromeDevTools@2812902))
* update Lighthouse to 13.4.0
([ChromeDevTools#2317](ChromeDevTools#2317))
([ffc6060](ChromeDevTools@ffc6060))


### 🛠️ Fixes

* enforce .gz instead of json.gz in performance tools
([ChromeDevTools#2305](ChromeDevTools#2305))
([b06e39b](ChromeDevTools@b06e39b))
* keep a still-open selected page instead of falling back to the first
page
([ChromeDevTools#2328](ChromeDevTools#2328))
([c645eee](ChromeDevTools@c645eee)),
closes
[ChromeDevTools#2304](ChromeDevTools#2304)
* keep page ids unique across browser reconnects
([ChromeDevTools#2345](ChromeDevTools#2345))
([3e8d922](ChromeDevTools@3e8d922))
* paginate page 0 in list_network_requests and list_console_messages
([ChromeDevTools#2359](ChromeDevTools#2359))
([d0025b3](ChromeDevTools@d0025b3))
* release held modifiers when press_key key event fails
([ChromeDevTools#2347](ChromeDevTools#2347))
([78ccb19](ChromeDevTools@78ccb19))
* report when the selected page was auto-replaced by the fallback
([ChromeDevTools#2308](ChromeDevTools#2308))
([2c16ac3](ChromeDevTools@2c16ac3)),
closes
[ChromeDevTools#2304](ChromeDevTools#2304)
* resolve page ids only among listed pages
([ChromeDevTools#2332](ChromeDevTools#2332))
([eb04951](ChromeDevTools@eb04951)),
closes
[ChromeDevTools#2304](ChromeDevTools#2304)
* **snapshot:** resolve element ids on the correct snapshot
([ChromeDevTools#2295](ChromeDevTools#2295))
([b703f2c](ChromeDevTools@b703f2c))
* **telemetry:** resolve enum values through nested schema wrappers
([ChromeDevTools#2315](ChromeDevTools#2315))
([c065fd9](ChromeDevTools@c065fd9))
* Wait until daemon is started
([ChromeDevTools#2327](ChromeDevTools#2327))
([ed7e95d](ChromeDevTools@ed7e95d))


### 📄 Documentation

* add Grok Build CLI configuration section
([ChromeDevTools#2294](ChromeDevTools#2294))
([aa4be07](ChromeDevTools@aa4be07))
* update memory leak debugging skill
([ChromeDevTools#2330](ChromeDevTools#2330))
([c1736a0](ChromeDevTools@c1736a0))


### ⚡ Performance

* concurrent I/O in Root Path Resolution
([ChromeDevTools#2279](ChromeDevTools#2279))
([b2c63e6](ChromeDevTools@b2c63e6))


### 🏗️ Refactor

* clean up McpContext getters
([ChromeDevTools#2340](ChromeDevTools#2340))
([5b33deb](ChromeDevTools@5b33deb))
* clean up more of the context interface
([ChromeDevTools#2335](ChromeDevTools#2335))
([9cd734b](ChromeDevTools@9cd734b))
* clean up page management
([ChromeDevTools#2333](ChromeDevTools#2333))
([16db01f](ChromeDevTools@16db01f))
* clean up page snapshot generation
([ChromeDevTools#2348](ChromeDevTools#2348))
([68cfce2](ChromeDevTools@68cfce2))
* make collectors work per page
([ChromeDevTools#2324](ChromeDevTools#2324))
([9bc61b4](ChromeDevTools@9bc61b4))
* move and rename files
([ChromeDevTools#2355](ChromeDevTools#2355))
([9c3542b](ChromeDevTools@9c3542b))
* move DevTools universe to McpPage
([ChromeDevTools#2341](ChromeDevTools#2341))
([c006c9b](ChromeDevTools@c006c9b))
* move remaining McpContext getters
([ChromeDevTools#2342](ChromeDevTools#2342))
([58ba174](ChromeDevTools@58ba174))
* remove isolated context getter
([ChromeDevTools#2336](ChromeDevTools#2336))
([8a4ddb3](ChromeDevTools@8a4ddb3))
* Use array instead of Map for idToClassKey
([ChromeDevTools#2321](ChromeDevTools#2321))
([ff53b7b](ChromeDevTools@ff53b7b))
* use helper for Dialog handle
([ChromeDevTools#2334](ChromeDevTools#2334))
([64005f9](ChromeDevTools@64005f9))
* use response page in formatting
([ChromeDevTools#2349](ChromeDevTools#2349))
([c53c1ec](ChromeDevTools@c53c1ec))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
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