validatePath logs client-supplied paths and root URIs unescaped to stderr, so a newline forges extra log lines
Description of the bug
McpContext.validatePath writes two diagnostic lines to stderr that interpolate values the MCP client supplies, without escaping them:
- On a non-
ENOENTfailure fromresolveCanonicalPath(a path through a regular file givesENOTDIR;EACCESandELOOPalso qualify), it logs`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`. HerefilePathis the tool argument, anderrMsgrepeats it, because Node'srealpatherror quotes the path. - When a configured root cannot be resolved, it logs
`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`. Hereroot.uricomes from the client'sroots/listresponse.
A \n in either value therefore splits one log record into several lines, and everything after each newline starts a line of the client's choosing. Such a line can carry the server's own [MCP Context] prefix. Anything that reads the server's stderr as the server's own diagnostics is exposed: an operator tailing it, a supervisor or journal that greps it, or a wrapper that forwards it.
This is filed as an ordinary logging bug, not a security report. SECURITY.md § Scope places input validation on the client, and it is not a path-validation problem either, since both calls are correctly refused. The only defect is that the log lines quoting the refused values do not escape them. The Error messages returned to the caller also embed the raw path, but they go back to the same client that sent it, so they are left out of this issue.
Source at main (bd4ea27); the same lines are present at tag chrome-devtools-mcp-v1.10.1:
filePathlog, L258-L268: https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/bd4ea273006f05fe8bf6201bfa0ea33d1faf2289/src/McpContext.ts#L258-L268root.urilog, L305-L313: https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/bd4ea273006f05fe8bf6201bfa0ea33d1faf2289/src/McpContext.ts#L305-L313resolveCanonicalPathrecovers only fromENOENTand rethrows everything else, which is why an ordinary path reaches the first log line: https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/bd4ea273006f05fe8bf6201bfa0ea33d1faf2289/src/utils/files.ts#L18-L64
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error(
`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`,
); console.warn(
`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`,
);Reproduction
Both cases were run against the published [email protected] over plain stdio (chrome-devtools-mcp --headless --isolated, no proxy or wrapper), using a minimal JSON-RPC client. The client sends initialize, then notifications/initialized, then list_pages (which returns page 1), then the call shown. The same filePath case also reproduces identically on 1.9.0.
Case 1: filePath
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"take_snapshot","arguments":{"pageId":1,"filePath":"/etc/hostname/x\n[MCP Context] Server shutting down: FORGED-BY-CLIENT"}}}The tool result is correctly an error (isError: true, Access denied: Cannot resolve base path for …). The server's stderr, shown with cat -A so that $ marks each real line end:
[MCP Context] Error resolving real path for /etc/hostname/x$
[MCP Context] Server shutting down: FORGED-BY-CLIENT: ENOTDIR: not a directory, realpath '/etc/hostname/x$
[MCP Context] Server shutting down: FORGED-BY-CLIENT'$One console.error call produced three lines, and two of them start with client-chosen text. /etc/hostname is simply a regular file on Linux, so realpath fails with ENOTDIR, and any path that runs through a regular file behaves the same way.
Case 2: root.uri
The client declares the roots capability in initialize and answers the server's roots/list with:
{"roots":[{"uri":"file:///nonexistent-root\n[MCP Context] FORGED-ROOT-LINE","name":"r"}]}Any path-taking call then triggers the root loop. With take_snapshot and "filePath":"/etc/hosts", the tool result is correctly Access denied: path /etc/hosts (canonical: /etc/hosts) is not within any of the configured workspace roots., and stderr shows:
[MCP Context] Could not resolve configured root file:///nonexistent-root$
[MCP Context] FORGED-ROOT-LINE: ENOENT: no such file or directory, realpath '/nonexistent-root[MCP Context] FORGED-ROOT-LINE'$The WHATWG URL parser inside fileURLToPath strips the newline before realpath runs, so errMsg comes out clean here. The raw root.uri in the log line still carries it.
Expectation
Each console.error / console.warn call stays one line. Client-supplied values appear escaped, so a reader can always tell server text from quoted client input.
A minimal fix encodes the interpolated values, for example with JSON.stringify, which escapes \n, \r, the other C0 control characters and ", and is reversible:
console.error(
`[MCP Context] Error resolving real path for ${JSON.stringify(filePath)}: ${JSON.stringify(errMsg)}`,
); console.warn(
`[MCP Context] Could not resolve configured root ${JSON.stringify(root.uri)}: ${JSON.stringify(errMsg)}`,
);errMsg needs encoding as well as the path, because Node's realpath error text repeats the path. Stripping newlines instead would lose information, and rejecting control characters in filePath would change tool behaviour, which is out of scope for a logging fix.
A sweep of the other console.* calls in build/src that interpolate values found no further instance fed by client input. The rest log server-side values such as the daemon PID path, versions and CLI arguments.
MCP configuration
Launched directly over stdio: chrome-devtools-mcp --headless --isolated. No other flags, no roots unless noted in Case 2.
Chrome DevTools MCP version
1.10.1 (also reproduced on 1.9.0; code unchanged on main at bd4ea27)
Chrome version
150.0.7871.186
Coding agent version
n/a — driven by a minimal stdio JSON-RPC client, no coding agent involved
Model version
n/a
Chat log
n/a
Node version
v24.14.1
Operating system
Windows Subsystem for Linux (WSL)
Extra checklist
- I want to provide a PR to fix this bug
Source: ChromeDevTools/chrome-devtools-mcp