
Cloud Vulnerability DB
A community-led vulnerabilities database
URLValidator, validateURL, sanitizeURL, and likewise FilePathValidator / validateFilePath, which never ran the sensitive-path check) did not receive the 1.4.0 fixes. Gap B duplicates GHSA-6xj4-4chr-656w, reported earlier, and is tracked there. Fixed in 1.4.2 (https://github.com/starman69/mcp-sanitizer/pull/14): every entry point now uses the same shared checks, enforced by an entry-point parity test.mcp-sanitizer 1.4.0 was published as the fix for two High advisories dated 2026-10-28:
GHSA-4mfg-r38w-w8fg — the URL sanitizer allowed loopback SSRF via an explicit port or bracketed IPv6.GHSA-7w82-6f9j-v9jp — the file path sanitizer permitted arbitrary user-home absolute paths.
Fix commit 2006858 changed src/utils/validation-utils.js, the config files, the README and tests. Two gaps remain in
1.4.1, the current latest:src/sanitizer/validators/url.js, exported from
the package root and documented in src/index.js. The fix did not reach it, so it still returns isValid: true for
the advisory's exact payloads — and requests to those URLs reach listeners bound only to loopback or link-local
addresses..ssh/id_rsa, .aws/credentials, .npmrc, .netrc, .docker/config.json, .env, .git-credentials) pass
sanitize(…, { type: 'file_path' }) with blocked: false, while sub/.env and all absolute spellings are blocked.
The fix comment at that site says it applies "regardless of where they sit (home dir, /tmp, cwd, relative paths)".
Neither gap is a new vulnerability class. Both are the published advisories' own classes on code paths the fix did
not cover, which is why this is filed as a new advisory rather than a comment on the existing ones.npm mcp-sanitizer@1.4.1 (latest), git tag v1.4.1 = dd2e765, node v26.8.1, macOS (Darwin arm64). The harness
binds its own listeners on 127.0.0.1 and ::1; no external network is used.
1.4.1.src/sanitizer/validators/url.js (added in 786be12, present through
1.3.3), by inspection. Not separately executed below 1.4.1, so that range is inference.2006858); 1.3.3 had no sensitive-path check at all.validateURLLocationsrc/index.js:68-71 exports the validator subsystem at the package root, and src/index.js:24-27 documents it:
// Use specific validators
const { validators } = require('mcp-sanitizer');
const urlValidator = new validators.URLValidator();
// Use convenience functions
const { validateFilePath } = require('mcp-sanitizer');validators/url.js classifies hosts by string pattern applied to parsedUrl.hostname: PRIVATE_IP_RANGES
(:73-90) and DANGEROUS_HOSTNAMES (:95-103), with _isIPAddress (:543-551) recognising only a bare literal.
Three consequences, all of them the defects named in GHSA-4mfg-r38w-w8fg:
/^::1$/ never matches [::1];[::ffff:127.0.0.1], because the mapped address is expressed as hex groups;0.0.0.0 is absent from PRIVATE_IP_RANGES.ipv4 (:73-82) entirely.allowPrivateIPs defaults to false (:55) and the range loops (:568-590) exist, so the intent to block private
and loopback targets is unambiguous; the patterns simply never see a normalised host.
src/utils/validation-utils.js:153-165:
// GHSA-7w82-6f9j-v9jp FIX: Block sensitive user-configuration files/directories
// regardless of where they sit (home dir, /tmp, cwd, relative paths). These hold
// SSH keys, cloud credentials, and tokens that must never be exposed via a tool.
const sensitiveFileFragments = [
'/.ssh/', '/.aws/', '/.gnupg/', '/.kube/', '/.config/',
'/.docker/', '/.npmrc', '/.netrc', '/.env', '/.git-credentials'
];
const sensitiveCheckPath = lowerPath.replace(/\\/g, '/');
for (const fragment of sensitiveFileFragments) {
if (sensitiveCheckPath.includes(fragment)) {
throw new Error('Access to sensitive configuration path not allowed');
}
}Every fragment begins with /, so a relative path can never contain one. The guard runs before the safe-root logic,
and the safe-root branch that allows relative paths under process.cwd() is not what admits these values — the
sensitive check is simply never reached for them.
evidence/harness.js is self-contained; evidence/output.txt is the recorded run. Section A calls the exported
validator and then performs a real GET to a listener bound only to the named host:
| input | isValid | fetch |
|---|---|---|
http://[::1]:<port>/ | true | 200 LOOPBACK-ONLY:::1 |
http://[::ffff:127.0.0.1]:<port>/ | true | 200 LOOPBACK-ONLY:127.0.0.1 |
http://[fe80::1]/ | true | ERR ECONNREFUSED (no link-local peer in this environment) |
http://0.0.0.0:<port>/ | true | 200 LOOPBACK-ONLY:127.0.0.1 |
http://127.0.0.1/ (control) | false | — |
http://localhost/ (control) | false | — |
The bracketed rows also return warnings: ["Security patterns detected: command_injection:shell_metacharacter…"] — the | ||
| validator notices the payload and still reports it as valid. | ||
Section B is the harness's sensitivity control: the fixed sanitize(url, { type: 'url' }) path blocks all four | ||
loopback URLs (URL points to private IP range: …), so the sanitizer, the listeners and the requests are all working, | ||
and only the validators/ path disagrees. | ||
Section C, through sanitize(path, { type: 'file_path' }): | ||
| input | blocked | sanitized |
| --- | --- | --- |
.ssh/id_rsa | false | .ssh/id_rsa |
.aws/credentials | false | .aws/credentials |
.npmrc | false | .npmrc |
.netrc | false | .netrc |
.docker/config.json | false | .docker/config.json |
.env | false | .env |
./.env | false | ./.env |
.git-credentials | false | .git-credentials |
sub/.env (control) | true | null |
x/../.env (control) | true | null |
/Users/x/.ssh/id_rsa (control) | true | null |
/etc/passwd (control) | true | null |
Section D: the exported file validator agrees — validateFilePath('.ssh/id_rsa') and validateFilePath('.env') return | ||
isValid: true, while the absolute and /etc/passwd forms return false. |
Gap A: a consumer that uses the documented validator API instead of the class, checks isValid, and then fetches the
URL, reaches loopback, IPv4-mapped or link-local services — the impact the published advisory describes, in the
project's own words ("a bypass here directly enables an attacker-controlled URL to reach internal services, leaking
their responses").
Gap B: a consumer that trusts blocked: false and reads the returned path reads a credential file relative to the
process working directory. That is the ordinary case for a project .env, .npmrc or .netrc; $HOME as the
working directory (common for an MCP server) also exposes .ssh, .aws and .docker. sub/.env being blocked while
./.env passes shows the guard is defeated by spelling alone.
Severity as found: High, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N (8.1), driven by gap A and matching the
severity of the advisories whose fixes are incomplete. Gap B on its own is Medium (about 5.3), since it additionally
requires the working directory to hold the secret. If triage scores the validator API as a secondary surface and keeps
only gap B, Medium is the expected outcome and I would not contest it.
Gap A — remove the duplicate implementation or make it delegate:
// src/sanitizer/validators/url.js
const { validateURLLocation } = require('../../utils/validation-utils');
// inside the host check, after `new URL(...)`:
validateURLLocation(parsedUrl);or port the same normalisation (strip [/], net.isIP, expand IPv6, treat 0.0.0.0/8 as local). Add
[::1], [::ffff:127.0.0.1], [fe80::1] and 0.0.0.0 to that validator's own test matrix; the 1.4.0 tests only
exercise the sanitize() path.
Gap B — match on segment boundaries after resolution:
const resolved = path.resolve(process.cwd(), normalizedPath).replace(/\\/g, '/').toLowerCase();
const sensitive = [/\/\.ssh(\/|$)/, /\/\.aws(\/|$)/, /\/\.gnupg(\/|$)/, /\/\.kube(\/|$)/, /\/\.docker(\/|$)/,
/\/\.config(\/|$)/, /\/\.npmrc$/, /\/\.netrc$/, /\/\.env(\.[^/]*)?$/, /\/\.git-credentials$/];
if (sensitive.some(re => re.test(resolved))) throw new Error('Access to sensitive configuration path not allowed');http://127.0.0.1.nip.io:<port>/) is still accepted by
the fixed sanitize() path, because the validator classifies literals and never resolves names. That is arguably by
design for a string-level validator, so it is recorded here rather than claimed.allowedDomains and blockedDomains appear only in src/config/; no code reads them, so the
STRICT and PRODUCTION policies advertise blockedDomains: ['localhost', '127.0.0.1', '0.0.0.0'] with no effect.test/ssrf-url-validation.test.js and test/path-traversal-validation.test.js cover the
sanitize() entry point only, which is why neither gap is caught.Analysis and reproduction were performed with the assistance of AI coding agents (OpenCode; Claude Code; OpenAI Codex/GPT). Every finding was verified by executing the code locally, with a negative control, against the version/commit named in the report.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."