SSRF and path traversal in file_upload tool: unvalidated file_url fetch, unsanitized fileName, arbitrary readBase64File (native host)

Author: trickyfalconCreated Aug 21, 2026Updated Sep 11, 2026

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:

  1. fetches file_url with no protocol/host validation (SSRF — including the cloud metadata endpoint),
  2. joins file_name into the temp dir with no traversal check,
  3. exposes readBase64File which reads any absolute path without the temp-dir restriction that cleanupFile enforces.

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 — prepareFiledownloadFile() (HIGH)

app/native-server/src/file-handler.ts:79-94:

typescript
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_url flows unmodified from the MCP tool argument (app/chrome-extension/entrypoints/background/tools/browser/file-upload.tsprepareFileFromRemote → native host) into fetch().
  • node-fetch v2 (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):

typescript
// 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 server

2. Path traversal on write — fileName (MEDIUM-HIGH)

Same function, line 87-88:

typescript
const finalFileName = fileName || this.generateFileName(fileUrl);
const filePath = path.join(this.tempDir, finalFileName);

fileName is not sanitized, and path.join resolves .. segments:

typescript
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+:

typescript
private async readBase64File(filePath: string): Promise<any> {
  ...
  const buffer = fs.readFileSync(filePath);   // no tempDir check

cleanupFile 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

  1. Validate file_url before fetching: allow only http:/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).
  2. Sanitize file_name: path.basename(fileName) and assert path.resolve(tempDir, finalFileName).startsWith(tempDir + path.sep) before writing.
  3. Guard readBase64File with the same startsWith(tempDir) check used by cleanupFile.

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.