Visitar URL original
feat: add --file-navigations flag to reject file: URL navigations by j0xh-sec · Pull Request #2853 · ChromeDevTools/chrome-devtools-mcp · GitHub
Skip to content

feat: add --file-navigations flag to reject file: URL navigations - #2853

Merged
OrKoN merged 4 commits into
ChromeDevTools:mainfrom
j0xh-sec:fix/enforce-roots-for-file-navigation
Oct 1, 2026
Merged

OrKoN merged 4 commits into
ChromeDevTools:mainfrom
j0xh-sec:fix/enforce-roots-for-file-navigation

Conversation

@j0xh-sec

@j0xh-sec j0xh-sec commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Adds --file-navigations (boolean, default true). When set to false, the navigation
tools reject file: URLs.

The check sits in validateUrl() next to the existing javascriptEvaluation and
categoryExtensions gates, so it covers every navigation entry point: new_page,
navigate_page and the slim navigate. It reads parsed.protocol and nothing else, so it
performs no filesystem access and no path resolution, and behaves the same whether the
browser runs on the MCP server's host or a different one.

fileNavigations is a required field on ValidateUrlOptions rather than optional, so a new
navigation tool that calls validateUrl() without passing it fails to compile.

The default of true preserves current behaviour. Note that when the flag is disabled the
server rejects every file: navigation, including a path inside a configured MCP root,
because it decides on the scheme rather than consulting roots.

Tests: flag parsing (both spellings and the negated form), validateUrl accept / reject /
other schemes unaffected, and per-tool rejection for new_page, navigate_page and slim
navigate. Full suite, typecheck and check-format pass locally.

This replaces the roots-based approach originally proposed in this PR. Per review feedback,
MCP roots restrict paths on the MCP server side and do not describe what the browser will
open — in particular when the browser runs on a different host, where resolving a local
path says nothing about the file the browser loads.

Reported to the Google OSS VRP (issue 567228129).

Comment thread src/tools/pages.ts Resolved

@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.

Generally, the filesystem roots apply to the MCP server code not to the browser actions. There are probably dozens of ways by which the web page can trigger file system access so I am not sure changing this makes sense. Previously we classified it as working according to the security.md (the client should validate tool calls https://github.com/ChromeDevTools/chrome-devtools-mcp/security/advisories/GHSA-gjv5-335h-4qcq).

I think this also does not work if the browser is on a different host relative to the MCP server. I do not have access to the VRP issue but I think it should be closed as a won't fix. What we could land instead is a CLI flag --file-navigations=boolean and reject the file navigations altogether without checking MCP server roots.

Add a boolean CLI flag, --file-navigations (default true). When set to
false, the navigation tools reject `file:` URLs. The check reads only the
URL scheme and performs no filesystem access and no path resolution, so it
behaves identically whether the browser runs on the same host as the MCP
server or a different one.

The check sits in validateUrl() next to the existing javascriptEvaluation
and categoryExtensions gates, so it covers every navigation entry point:
new_page, navigate_page and the slim navigate tool. fileNavigations is a
required field on ValidateUrlOptions rather than optional, so a new
navigation tool that omits it fails to compile.

The default of true preserves current behaviour. Operators who do not want
the browser opening local files pass --file-navigations=false.

This replaces the roots-based approach previously proposed in this PR.
@j0xh-sec
j0xh-sec force-pushed the fix/enforce-roots-for-file-navigation branch from ad5e0b0 to 8d60bee Compare September 30, 2026 14:26
@j0xh-sec j0xh-sec changed the title fix: enforce workspace roots for file:// URLs in navigation tools feat: add --file-navigations flag to reject file: URL navigations Sep 30, 2026
@j0xh-sec

Copy link
Copy Markdown
Contributor Author

Generally, the filesystem roots apply to the MCP server code not to the browser actions. There are probably dozens of way by which the web page can trigger file system access so I am not sure changing this makes sense. Previously we classified it as working according to the security.md (the client should validate tool calls https://github.com/ChromeDevTools/chrome-devtools-mcp/security/advisories/GHSA-gjv5-335h-4qcq).

I think this also does not work if the browser is on a different host relative to the MCP server. I do not have access to the VRP issue but I think it should be closed as a won't fix. What we could land instead is a CLI flag --file-navigations=boolean and reject the file navigations altogether without checking MCP server roots.

Thanks for the review @OrKoN . You're right on every point, so I've dropped the roots approach and implemented the CLI flag you suggested.

What the VRP issue describes

You can't see the tracker, so here it is in full.

With a client that negotiates roots, the file-aware tools enforce them. take_screenshot / take_snapshot refuse a filePath outside the roots:

take_screenshot { "filePath": "<cwd>/outside/x.png" }
-> Error: Access denied: path <cwd>/outside/x.png is not within any of the
   configured workspace roots.

new_page / navigate_page only called validateUrl(), which checks the scheme and not the path. So with those same roots configured and actively refusing the calls above:

new_page { "url": "file://<cwd>/outside/secret.txt" }   -> page created, no error
evaluate_script { "function": "() => document.body.innerText" }
-> "CANARY_SECRET_a7f3e91c_DO_NOT_LEAK"

take_snapshot returns the same content as StaticText. file:///etc/passwd and directory listings (file:///, file:///home/) behave the same way.

I reported it because SECURITY.md states:

If the client specifies them, the MCP server will check the roots when accessing files.

In fairness, the same paragraph adds that you treat MCP roots issues as low severity because roots are optional, so the severity cap was documented and I should have weighted it higher before filing.

That is the whole issue: a caller who can issue one tool call reads any file the server's user can read. It requires a tool call the client is expected to have vetted, and it goes no further than reading local files.

On your points

I agree with all four.

  • Roots restrict paths on the MCP server side. Navigation is a browser action. Enforcing one through the other conflates two layers.
  • The remote-browser case decides it. With --browserUrl pointing at another host, my validatePath() call resolved a path on the MCP server's filesystem, which tells you nothing about what the browser will open. That alone rules the approach out.
  • Your SECURITY.md position that the calling agent validates tool calls does cover this, and GHSA-gjv5-335h-4qcq already classifies the class. Closing the VRP issue as a won't fix is reasonable.
  • On the dozens of other ways a page can reach the filesystem: agreed, and this flag does not address them. It gates one thing only — whether the server accepts a file: URL as a navigation target — which is the part the server decides.

What this PR now contains

I removed the roots check, the Context.validateUrl method, and the validatePath exposure on Context. The PR no longer touches McpContext.ts or ToolDefinition.ts.

I added:

  • --file-navigations (also accepted as --fileNavigations), boolean, default true. Declared in mcpOptions, so it picks up the usual negation forms (--no-file-navigations, --file-navigations=false) and docs:generate documents it in docs/configuration.md.

  • fileNavigations on ValidateUrlOptions, checked in validateUrl() beside the existing gates:

    if (fileNavigations === false && parsed.protocol === 'file:') {
      throw new Error(
        `Navigating to file: URLs is not allowed when --file-navigations is disabled.`,
      );
    }
  • The option threaded through all three navigation entry points: new_page, navigate_page, and the slim navigate.

The check reads parsed.protocol and nothing else, so it performs no filesystem access and behaves the same for a local or a remote browser.

Two things worth calling out:

  • The flag rejects every file: navigation when disabled, including a path inside a configured root. That follows from rejecting on scheme rather than consulting roots, which is what you asked for, but it is a behaviour change for anyone who both configures roots and navigates to files inside them.
  • On @Lightning00Blade's earlier point that the check is easy to forget: I made fileNavigations a required field on ValidateUrlOptions rather than optional, so a new navigation tool that calls validateUrl() without passing it fails to compile.

The default

I chose true, which keeps current behaviour and makes the flag opt-in. If you'd rather make file: navigation opt-in instead and default to false, say so and I'll flip it — it's a one-line change plus test updates.

Verification

7 tests added: flag parsing (both spellings and the negated form), validateUrl accept / reject / leaves other schemes alone, and per-tool rejection for new_page, navigate_page and slim navigate, asserting goto never runs.

I also ran the reporting client against the built server. With no flag, new_page on a file: URL still returns the file contents. With --no-file-navigations, new_page and navigate_page both return the error above, and http:// navigation keeps working in both modes.

Comment thread src/utils/url.ts Resolved
@OrKoN

OrKoN commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Thanks!

If the client specifies them, the MCP server will check the roots when accessing files.

To clarify: in this case, the MCP server does not access the files, the browser does. The file URL might have been already loaded in the browser before the MCP server connects to the (potentially) browser so enforcing MCP roots on the browser end does not really work that well.

… are off

`view-source:` wraps another URL and Chrome resolves the inner target, so
`view-source:file:///etc/passwd` still read the file with
`--file-navigations=false`: `new URL()` reports its protocol as
`view-source:`, and the `file:` check did not match.

targetsFileScheme() unwraps any `view-source:` prefixes and re-parses the
inner URL, so an uppercase inner scheme (`view-source:FILE:///etc/passwd`)
and nested wrapping both normalise to `file:`. A string prefix match would
miss the uppercase form. `view-source:` of http and https is unaffected.
The flag restricts navigations the server performs. It does not affect
pages the browser already had open when the server connected, and the
browser can reach the filesystem by other means. Say so in the flag
description so it is not mistaken for filesystem confinement.
@j0xh-sec

Copy link
Copy Markdown
Contributor Author

That's a fair correction, and I tested it rather than just taking the point.

I launched Chrome separately with file:///…/outside/secret.txt already open, then connected the MCP server to it with --browserUrl, roots set to a different directory, and --no-file-navigations:

list_pages      -> 1: secret.txt (file:///…/outside/secret.txt) [selected]
evaluate_script -> "CANARY_SECRET_a7f3e91c_DO_NOT_LEAK"
take_snapshot   -> RootWebArea url="file:///…/outside/secret.txt"

So the pre-existing page is reachable, the flag does not touch it, and roots have no bearing on it. You're right that the SECURITY.md sentence I quoted is about the server accessing files, and that is not what happens here — the browser holds the page and the server reads what the browser already has.

I've taken that into the flag's own description, so it doesn't get mistaken for a sandbox:

This restricts navigations the server performs. It is not a filesystem sandbox: it does not affect pages the browser already had open when the server connected, and the browser can reach the filesystem by other means. Use OS sandboxing for full filesystem confinement.

Happy to reword that if you'd put it differently.

That leaves the flag doing one narrow thing: refusing a file: navigation the server is asked to perform. That seems worth having on its own terms, but it is not a boundary and the description now says so.

@OrKoN
OrKoN enabled auto-merge October 1, 2026 06:08

@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.

please run npm run gen to regenerate generated code

auto-merge was automatically disabled October 1, 2026 07:31

Head branch was pushed to by a user without write access

@OrKoN
OrKoN enabled auto-merge October 1, 2026 07:34
@OrKoN
OrKoN added this pull request to the merge queue Oct 1, 2026
Merged via the queue into ChromeDevTools:main with commit 54cae57 Oct 1, 2026
20 checks passed
OrKoN added a commit that referenced this pull request Oct 15, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.11.0](chrome-devtools-mcp-v1.10.1...chrome-devtools-mcp-v1.11.0)
(2026-10-15)


### 🎉 Features

* add --file-navigations flag to reject file: URL navigations
([#2853](#2853))
([54cae57](54cae57))
* Add analyze_heapsnapshot_contexts MCP tool
([#2791](#2791))
([190aeee](190aeee))
* add ignoreCache option to performance_start_trace
([#2931](#2931))
([3467d40](3467d40))
* add maxNameLength truncation option to compare_heapsnapshots
([#3098](#3098))
([1e1c53e](1e1c53e))
* Add pagination to compare_heapsnapshots
([#3140](#3140))
([daebfd1](daebfd1))
* **config:** add ConfigWatcher to watch config file changes
([#3139](#3139))
([9171f77](9171f77))
* discover the config file in predefined locations
([#2876](#2876))
([02c0112](02c0112))
* implement ffmpeg-free screencast tools enabled by default
([#3123](#3123))
([8953ea5](8953ea5))
* reduce StaticText noise in text snapshots
([#2895](#2895))
([4441325](4441325))
* report pages opened by input actions
([#2679](#2679))
([28fb230](28fb230))
* resolve config defaults after merging CLI and config file
([#2849](#2849))
([bdeb70d](bdeb70d))
* return failing audits and DOM nodes inline from lighthouse_audit
([#2930](#2930))
([241fba5](241fba5))
* skip more redundant data in text snapshots
([#2896](#2896))
([b2f522c](b2f522c))
* support debugging WebMCP tools in third-party developer tools
([#2900](#2900))
([0db519f](0db519f))
* support emulating additional CSS media features
([#2929](#2929))
([69524f0](69524f0))
* support evaluating scripts from local files
([#2606](#2606))
([9cf47a4](9cf47a4))
* Support truncation in get_heapsnapshot_details
([#3137](#3137))
([f827e06](f827e06))
* Truncate long names by default to 100 characters
([#2893](#2893))
([5ddb0a3](5ddb0a3))


### 🛠️ Fixes

* account for scroll offsets in downscaled screenshots
([#2685](#2685))
([830c693](830c693))
* allow --categoryExtensions when connecting to an existing browser
([#3041](#3041))
([d250bc5](d250bc5))
* allow stopping the CLI if command fails
([#3088](#3088))
([77e238e](77e238e)),
closes
[#2982](#2982)
* avoid runtime title fetches
([#2883](#2883))
([04195d1](04195d1))
* better config support for chrome-devtools CLI
([#2819](#2819))
([9624e64](9624e64))
* clamp async stack trace separator repeat count
([#3035](#3035))
([93e06ab](93e06ab)),
closes
[#3031](#3031)
* **cli:** pass positional values starting with `-` through to tools
([#3084](#3084))
([9de05ae](9de05ae)),
closes
[#2984](#2984)
* **collectors:** ignore transient target errors when subscribing to
service workers
([#2888](#2888))
([551dd6c](551dd6c))
* **daemon:** bound browser shutdown and avoid watchFile timeout on stop
([#2887](#2887))
([f3db5e7](f3db5e7))
* **daemon:** keep fallback socket in private runtime dir
([#2766](#2766))
([871a37e](871a37e))
* **daemon:** verify process identity before treating PID file as active
([#3109](#3109))
([b608bed](b608bed))
* do not close a healthy browser when a tool call outlasts 60s
([#3149](#3149))
([39c7db1](39c7db1))
* ensure aggregate IDs in heap snapshots are deterministic
([#2923](#2923))
([e918c47](e918c47))
* escape client-supplied values in McpContext logs and errors
([#2832](#2832))
([efb0d02](efb0d02)),
closes
[#2831](#2831)
* fail fast on tool calls stuck after a dead browser connection
([#2699](#2699))
([2a7ef50](2a7ef50))
* harden third-party tool result serialization
([#2892](#2892))
([4418a7b](4418a7b))
* honor explicit --allowUnrestrictedPaths=false in CLI mode
([#3040](#3040))
([61fc362](61fc362))
* include errors in slim responses
([#3085](#3085))
([ee38097](ee38097))
* **input:** reject multiple file paths on single-file input in
upload_file
([#2995](#2995))
([#3135](#3135))
([e650843](e650843))
* list fresh page titles
([#3103](#3103))
([e717082](e717082))
* **memory:** reject contradictory pagination options in
take_heapsnapshot
([#3002](#3002))
([#3131](#3131))
([508126c](508126c))
* **network:** enforce URL policy across redirects
([#2767](#2767))
([274cd4c](274cd4c))
* pass categoryExtensions to the target filter when connecting
([#3047](#3047))
([4e90238](4e90238))
* **performance:** report an error when performance_stop_trace is called
with no active trace
([#3001](#3001))
([#3126](#3126))
([e170384](e170384))
* preserve HeapSnapshotManager across browser reconnects
([#3025](#3025))
([64388c6](64388c6))
* propagate failed responseFilePath write error instead of reporting
body unavailable
([#2957](#2957))
([#3119](#3119))
([3cee3bd](3cee3bd))
* **pwa:** wait for initial navigation on launch and retry transient
install error
([#2889](#2889))
([63010cb](63010cb))
* record screencast at the page's device pixel size
([#3155](#3155))
([62e2aa4](62e2aa4))
* reject --blockedUrlPattern/--allowedUrlPattern hostname regexp groups
([#2796](#2796))
([ae0aaef](ae0aaef))
* reject JSON arrays as 3p and WebMCP tool params
([#2785](#2785))
([c14e616](c14e616))
* reject named groups in URL patterns
([#2857](#2857))
([5b2c6f9](5b2c6f9))
* reject non-positive --viewport dimensions
([#2781](#2781))
([0ef528a](0ef528a))
* reject non-positive resize_page dimensions
([#2784](#2784))
([6ab6d35](6ab6d35))
* reject unrestricted paths with an explicit workspace
([#2858](#2858))
([45c1c6c](45c1c6c))
* report unknown page id before last-page guard in close_page
([#3042](#3042))
([bdc7e24](bdc7e24))
* round lighthouse category scores and show unscored categories as n/a
([#2924](#2924))
([7007680](7007680)),
closes
[#2915](#2915)
[#2916](#2916)
* set empty filePath to undefined instead of empty string
([#2836](#2836))
([adfc1ff](adfc1ff)),
closes
[#2825](#2825)
* surface WebMCP tool exceptions in execute_webmcp_tool
([#3107](#3107))
([d80d22e](d80d22e)),
closes
[#2975](#2975)
* url pattern partial ipv4
([#2859](#2859))
([c87d190](c87d190))


### 📄 Documentation

* clarify Antigravity MCP setup for VS Code extension
([#2855](#2855))
([12e13ca](12e13ca))
* clarify scope of --no-javascript-evaluation
([#3100](#3100))
([0c67cd5](0c67cd5))
* correct link to configuration
([#3086](#3086))
([bbb1e36](bbb1e36))
* document trusted host environment in SECURITY.md
([#3125](#3125))
([1352207](1352207))
* fix inaccurate examples in skills and CLI docs
([#2922](#2922))
([ec372b0](ec372b0)),
closes
[#2906](#2906)
[#2907](#2907)
[#2908](#2908)
[#2910](#2910)
[#2912](#2912)
[#2913](#2913)
[#2918](#2918)
[#2919](#2919)
[#2920](#2920)
[#2921](#2921)
* fix some reported issues
([#3099](#3099))
([d5d2f08](d5d2f08))
* include serviceWorkerId parameter for evaluate_script
([#3015](#3015))
([#3129](#3129))
([0623434](0623434))
* mention heapsnapshot being cached between runs
([#3045](#3045))
([f08dbe1](f08dbe1)),
closes
[#2976](#2976)
* **skills:** remove invalid outputDir flag from lighthouse_audit calls
([#3013](#3013))
([#3130](#3130))
([7754226](7754226))
* update documentation and SKILL for get_css_styles tool
([#2824](#2824))
([3c12a91](3c12a91))


### ⚡ Performance

* **cli:** re-use MCPServer.from helpers for daemon
([#2804](#2804))
([89eb3d5](89eb3d5))
* Load heap snapshots in parallel in compare_heapsnapshots
([#3023](#3023))
([20c1391](20c1391))


### 🏗️ Refactor

* centralize config defaults and conflict checks
([#2753](#2753))
([a739ba8](a739ba8))
* create all tools and register per slim mode
([#2874](#2874))
([99847a7](99847a7))
* enable categories by default
([#2811](#2811))
([e33ee48](e33ee48))
* extract CLI examples and conflict list in mcp-options
([#2848](#2848))
([45000d9](45000d9))
* extract config parsing logic in a class
([#2861](#2861))
([e4168d0](e4168d0))
* extract worker logic into McpWorker
([#2800](#2800))
([77165c6](77165c6))
* group Puppeteer and tool options
([#2875](#2875))
([699c4d5](699c4d5))
* lazily initialize McpPage from Target and enumerate targets in
McpContext
([#2812](#2812))
([545cbe3](545cbe3))
* use default Error for toolSchema
([#2822](#2822))
([8b2768d](8b2768d))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: Alex Rudenko <OrKoN@users.noreply.github.com>
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.

3 participants