SSRF and path traversal in file_upload tool: unvalidated file_url fetch, unsanitized fileName, arbitrary readBase64File (native host)
Hi hangwin,
I found a few related issues in the file upload path of the MCP server. I'm a security researcher focused on AI/MCP application security. I'd love to coordinate a fix and a CVE; happy to help with a patch.
TL;DR
The file_upload MCP tool passes LLM-supplied arguments (file_url,
file_name) to the native Node.js host, which:
- fetches
file_urlwith no protocol/host validation (SSRF — including the cloud metadata endpoint), - joins
file_nameinto the temp dir with no traversal check, - exposes
readBase64Filewhich reads any absolute path without the temp-dir restriction thatcleanupFileenforces.
Because file_upload is an MCP tool, its arguments are produced by the LLM
— and the LLM is influenced by page content it is browsing. A malicious or
attacker-influenced page (prompt injection) can steer the tool's arguments,
so "trusted local input" does not fully bound the threat. The same input
class was recently fixed in the MCP TypeScript SDK (CVE-2025-66414 /
CVE-2025-66416, DNS rebinding on streamable-HTTP) and in the Go/Java MCP
SDKs in 2025–2026.
1. SSRF — prepareFile → downloadFile() (HIGH)
app/native-server/src/file-handler.ts:79-94:
private async downloadFile(fileUrl: string, fileName?: string): Promise<any> {
const response = await fetch(fileUrl); // no URL validation
...
const finalFileName = fileName || this.generateFileName(fileUrl);
const filePath = path.join(this.tempDir, finalFileName);
const buffer = await response.buffer();
fs.writeFileSync(filePath, buffer);file_urlflows unmodified from the MCP tool argument (app/chrome-extension/entrypoints/background/tools/browser/file-upload.ts→prepareFileFromRemote→ native host) intofetch().node-fetchv2 (this project's dependency) does not restrict schemes to http/https by policy here, so the fetch call itself performs no host, port, or protocol allow-listing.
Impact (host where the native messaging host runs = user's machine):
http://169.254.169.254/latest/meta-data/iam/security-credentials/→ cloud instance credential disclosure when running in AWS (also Azure/GCP equivalents),http://127.0.0.1:<port>→ discovery/read of local services (Docker API 2375, kubectl proxy, local admin panels, npm/pip registries with creds),- response body is written to a predictable temp file and its path returned to the LLM context, so fetched data can be exfiltrated by the model.
Repro (minimal, no Chrome required) — I have a full local PoC
(happy to share it privately; it drives file-handler.ts directly):
// start a local HTTP server on 127.0.0.1:PORT serving a marker payload
const fh = new FileHandler();
await fh.handleFileRequest({
action: "prepareFile",
fileUrl: "http://127.0.0.1:PORT/metadata",
fileName: "ssrf.bin",
});
// -> { success: true, filePath: "<tmp>/chrome-mcp-uploads/ssrf.bin" }
// containing the marker payload served by the local server2. Path traversal on write — fileName (MEDIUM-HIGH)
Same function, line 87-88:
const finalFileName = fileName || this.generateFileName(fileUrl);
const filePath = path.join(this.tempDir, finalFileName);fileName is not sanitized, and path.join resolves .. segments:
await fh.handleFileRequest({
action: "prepareFile",
fileUrl: "http://127.0.0.1:PORT/x",
fileName: "../../tmp/attacker-controlled", // or absolute: "/tmp/x"
});
// -> file written outside chrome-mcp-uploads/Verified locally: fileName: "../poc-outside-temp" writes to
os.tmpdir()/poc-outside-temp. Combined with #1, the attacker controls
both the content and the destination path (within what the Node
host process can write), enabling overwrite of app config/data files.
3. Arbitrary file read — readBase64File (MEDIUM, defense-in-depth)
app/native-server/src/file-handler.ts:171+:
private async readBase64File(filePath: string): Promise<any> {
...
const buffer = fs.readFileSync(filePath); // no tempDir checkcleanupFile guards with filePath.startsWith(this.tempDir) —
readBase64File does not. Today the extension only sends
prepareFile/cleanupFile/analyzeTrace, so this is currently a
latent/defense-in-depth gap (the handleFileRequest dispatch accepts the
action from any native-messaging payload). Flagging because the dispatch
is the shared entry point; if any future client action (or a debug
endpoint) sends readBase64File, it's an unbounded local file read.
Suggested fixes
- Validate
file_urlbefore fetching: allow onlyhttp:/https:, resolve hostname, block loopback/link-local/CGNAT ranges (127.0.0.0/8,169.254.0.0/16,10/8,172.16/12,192.168/16,::1,fc00::/7,fe80::/10), or at minimum keep an explicit allow-list and reject non-allow-listed hosts. Re-resolve after redirect to defeat DNS-rebinding (same fix class as CVE-2025-66414/66416 in the MCP TS SDK). - Sanitize
file_name:path.basename(fileName)and assertpath.resolve(tempDir, finalFileName).startsWith(tempDir + path.sep)before writing. - Guard
readBase64Filewith the samestartsWith(tempDir)check used bycleanupFile.
Credit / disclosure
Happy to be credited in the advisory/release notes as: [Mo] (@trickyfalcon, https://trickyfalcon.com). Please let me know your preferred private channel (GitHub private vulnerability reports work fine) and your typical disclosure window.
Thanks for building mcp-chrome — it's one of the most useful browser MCP servers out there.
Source: hangwin/mcp-chrome