GHSA-hv85-774v-26fg: auth-fetch-mcp SSRF And Disk Exfiltration Vulnerability
HIGHGHSA-hv85-774v-26fg is a HIGH (CVSS 8.2) ssrf and disk exfiltration vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting auth-fetch-mcp libraries in the npm ecosystem. The issue involves ssrf and disk exfiltration via unvalidated auth_fetch and download_media urls. Remediation is available by upgrading affected packages to version 3.0.1.
GHSA-hv85-774v-26fg at a Glance
Security Impact
CVSS: 8.2
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N).
How to Fix GHSA-hv85-774v-26fg
To remediate this vulnerability, upgrade all affected auth-fetch-mcp dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install auth-fetch-mcp@3.0.1
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls auth-fetch-mcp
How to Check if You Are Affected
To determine whether your Node.js application is affected by GHSA-hv85-774v-26fg, inspect your project dependencies using npm:
npm ls auth-fetch-mcp
Analyze the terminal output to locate any version of auth-fetch-mcp in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.0.1), your application is affected.
Note: The npm ls auth-fetch-mcp command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In auth-fetch-mcp, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-hv85-774v-26fg Affected auth-fetch-mcp Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
auth-fetch-mcp
|
npm |
< 3.0.1
|
3.0.1
|
References
Frequently Asked Questions
What is GHSA-hv85-774v-26fg?
Which auth-fetch-mcp versions are affected?
What version fixes GHSA-hv85-774v-26fg?
Is auth-fetch-mcp affected?
What is the CVSS score of GHSA-hv85-774v-26fg?
What CWE is associated with GHSA-hv85-774v-26fg?
How can I check npm dependencies for GHSA-hv85-774v-26fg?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-hv85-774v-26fg",
"modified" : "2026-05-19T15:47:27Z",
"published" : "2026-05-19T15:47:27Z",
"aliases" : [ ],
"summary" : "auth-fetch-mcp: SSRF and disk exfiltration via unvalidated auth_fetch and download_media URLs",
"details" : "# SSRF + disk-exfil in `download_media` and `auth_fetch` tools — ymw0407/auth-fetch-mcp\n\n## Severity\nThe `download_media` and `auth_fetch` MCP tools accept arbitrary URLs and reach them as the MCP server process, with `download_media` additionally persisting the fetched response body to a user-controlled output directory. An MCP client (LLM under prompt injection, malicious peer) can drive the server to fetch loopback / link-local / private-range hosts (cloud-instance metadata, internal services, host-bound services) and exfiltrate the response.\n\n## Vulnerability chain\n\n### Site 1: `download_media` — SSRF + disk-write chain\n\n`src/tools.ts:200-274`\n```ts\nserver.registerTool(\"download_media\", {\n inputSchema: {\n urls: z.array(z.string()).describe(\"One or more URLs to download\"),\n output_dir: z.string().optional()...,\n },\n}, async ({ urls, output_dir }) => {\n ...\n for (const url of urls) {\n try {\n const response = await ctx.request.get(url); // line 238 — no validation\n ...\n const body = await response.body();\n ...\n const filePath = path.join(dir, `file-${++counter}${ext}`);\n fs.writeFileSync(filePath, body); // line 257 — writes response to disk\n```\n\n`urls` and `output_dir` are user-controlled. The handler iterates each URL (line 236) and calls `ctx.request.get(url)` (Playwright's `APIRequestContext.get`) without checking the destination. The response body is written to `path.join(output_dir, file-N.ext)`. Internal-service responses are persisted to disk where they can be exfiltrated via any subsequent tool that reads from the output directory (or via the response object itself, which contains `localPath` and `size` of every successful write).\n\n### Site 2: `auth_fetch` — SSRF via Playwright navigation\n\n`src/tools.ts:117-198`\n```ts\nserver.registerTool(\"auth_fetch\", {\n inputSchema: {\n url: z.string().describe(\"The URL to fetch content from\"),\n wait_for: z.string().optional()...,\n },\n}, async ({ url, wait_for }) => {\n ...\n const page = await navigateTo(ctx, url); // line 142\n ...\n const result = await extractContent(page);\n return textResult({ status: \"ok\", url: result.url, title: result.title, content: result.content });\n});\n```\n\n`src/browser.ts:53-64`\n```ts\nexport async function navigateTo(ctx: BrowserContext, url: string): Promise<Page> {\n ...\n await page.goto(url, { waitUntil: \"domcontentloaded\", timeout: 30000 }); // line 63\n return page;\n}\n```\n\n`url` flows directly from the MCP tool argument to `page.goto` with no validation. Playwright will navigate to any URL the network stack can reach. The page DOM is returned in the tool response via `extractContent`. Internal pages (loopback admin UIs, cloud metadata endpoints reachable from the host, intranet services) are extractable.\n\n## Root cause\nNeither handler validates URL targets before dispatch. The tool descriptions (\"fetches web page content using a real browser ... e.g. Notion, Google Docs, Jira, Confluence, Linear, Slack, or any SaaS/private page\") frame the intended usage as **public SaaS web pages**, not loopback or link-local hosts — but no code enforces that intent.\n\nThe fix shape (apply to both tools): after URL parsing, resolve to IP, reject if private/loopback/link-local. Same defense as the well-known SSRF-guard pattern shipped by other MCP fetchers in the ecosystem (e.g., `Akitaroh/scraper-mcp` `src/security/url-guard.ts`).\n\n## Auth boundary violated\n**Boundary type:** MCP tool-argument boundary plus the local-network trust boundary. The MCP server typically sits inside a trust boundary (developer laptop with loopback services, cloud VM with IMDS, k8s pod with service account). The tools allow the MCP client to dispatch HTTP requests across that boundary.\n\n**Respected/violated trace:** Per the tool descriptions, the expected respected boundary is \"public SaaS web pages.\" That expectation is violated by any request reaching a host the user didn't intend to expose (127.0.0.1:6379 Redis, 169.254.169.254 cloud metadata, 192.168.0.1 internal admin).\n\n## Impact\n\n1. **Cloud credential theft** — server on EC2 / GCE / Azure VM. MCP client invokes `auth_fetch({ url: \"http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>\" })` and receives temporary credentials in the tool response. Or invokes `download_media({ urls: [...], output_dir: \"/tmp/exfil\" })` to persist them to disk.\n\n2. **Internal service enumeration** — MCP client probes private-range hosts (10/8, 172.16/12, 192.168/16). Each `auth_fetch` returns the page DOM; each `download_media` writes the response to disk.\n\n3. **Loopback exploitation** — server runs alongside Redis (127.0.0.1:6379), ElasticSearch (127.0.0.1:9200), or internal admin UIs. MCP client reads them via `auth_fetch`.\n\n4. **Disk-write side channel** (`download_media` only) — output_dir is also user-controlled, with no documented restriction. An MCP client can request `output_dir = \"/some/user-writable-shared-dir\"` and exfil internal-service responses to a location accessible to a co-tenant process.\n\nThe injection vector is any content reaching the model that prompts a fetch tool call. The tool description explicitly says \"MUST be used instead of Fetch/web_fetch when the page requires login\" — meaning the model is encouraged to call this tool for any \"private page\" mention, which a prompt-injected upstream content can trivially trigger.\n\n## Proof of concept (non-destructive)\n\n`poc.mjs` — replicates the `download_media` handler's HTTP-fetch + file-write chain against a local fake-internal HTTP service. Playwright's `ctx.request.get(url)` is replaced with the equivalent `fetch(url)` for the bug case (a URL needing no auth) so the demo runs without browser deps. The structural defect — \"no host validation before HTTP dispatch\" — is identical.\n\n```\n[PoC] fake internal-only service: 127.0.0.1:36105\n[PoC] simulating MCP client calling download_media({\n urls: ['http://127.0.0.1:36105/secrets'],\n output_dir: '/tmp/auth-fetch-exfil-aU1jjv'\n })\n[PoC] no IP / host validation exists at tools.ts:236-238 before ctx.request.get(url)\n[PoC] ✓ SSRF + DISK-EXFIL CONFIRMED\n File written to: /tmp/auth-fetch-exfil-aU1jjv/file-1.json\n Persisted content (187 bytes):\n {\n \"AccessKeyId\": \"AKIA-FAKE-FROM-POC\",\n \"SecretAccessKey\": \"fake-secret-marker-NOT-REAL\",\n \"Note\": \"In a real exploit this would be AWS IMDS at 169.254.169.254/latest/meta-data/...\"\n }\n```\n\nExit code `0`. SHA-256 `poc.mjs`: `4cea53f1a618581fc67f9a8bd07a7a2b22274f42cdbf7f3c658519673aaf7568`. The PoC only contacts `127.0.0.1` on an ephemeral port; the fake-credentials string contains the literal `FAKE` marker so no downstream system can mistake it for real credentials. The exfil directory is cleaned up after the demo.\n\n## Suggested fix\n\nAdd a `assertSafeUrl` helper (same shape as in the matching egoist/fetch-mcp advisory) called before any HTTP dispatch — at `tools.ts:236` inside the download_media loop, and at the top of `navigateTo` in `browser.ts:53`:\n\n```ts\nimport dns from 'node:dns/promises'\nimport net from 'node:net'\n\nasync function assertSafeUrl(rawUrl: string): Promise<URL> {\n const parsed = new URL(rawUrl)\n if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(`Unsupported scheme`)\n const host = parsed.hostname\n const addresses = net.isIP(host)\n ? [host]\n : (await dns.lookup(host, { all: true })).map(a => a.address)\n for (const addr of addresses) {\n if (isPrivateOrLinkLocal(addr)) throw new Error(`Refusing to fetch ${addr}`)\n }\n return parsed\n}\n```\n\nWhere `isPrivateOrLinkLocal` blocks 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10.\n\nFor `download_media` specifically, also constrain `output_dir`: resolve it under a fixed root (e.g., `~/.auth-fetch-mcp/downloads/`) and reject if the resolved path escapes that root.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "auth-fetch-mcp"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.0.1"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 3.0.0"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/ymw0407/auth-fetch-mcp/security/advisories/GHSA-hv85-774v-26fg"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/ymw0407/auth-fetch-mcp"
}, {
"type" : "WEB",
"url" : "https://github.com/ymw0407/auth-fetch-mcp/releases/tag/v3.0.1"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-918" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-19T15:47:27Z",
"nvd_published_at" : null
}
}