Vulnerability DatabaseGHSA-v25x-53wq-6r99

GHSA-v25x-53wq-6r99: 
JavaScript vulnerability analysis and mitigation

Maintainer triage: Accepted for Gap A: the exported validators (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.

Summary

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:
  1. The package ships a second implementation of the same checks, 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.
  2. The new sensitive-path denylist tests leading-slash substrings, so relative spellings of the same credential paths (.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.

Environment

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.

Affected versions

  • Verified by execution: 1.4.1.
  • Gap A: present in every version that ships 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.
  • Gap B: introduced by the 1.4.0 fix (2006858); 1.3.3 had no sensitive-path check at all.

Details

Gap A — the fix was applied to one of two implementations of validateURLLocation

src/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:

  • a bracketed IPv6 hostname keeps its brackets, so /^::1$/ never matches [::1];
  • no IPv4 rule can match [::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.

Gap B — the sensitive-path denylist only matches paths with a leading separator

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.

Reproduction

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:

inputisValidfetch
http://[::1]:<port>/true200 LOOPBACK-ONLY:::1
http://[::ffff:127.0.0.1]:<port>/true200 LOOPBACK-ONLY:127.0.0.1
http://[fe80::1]/trueERR ECONNREFUSED (no link-local peer in this environment)
http://0.0.0.0:<port>/true200 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' }):
inputblockedsanitized
---------
.ssh/id_rsafalse.ssh/id_rsa
.aws/credentialsfalse.aws/credentials
.npmrcfalse.npmrc
.netrcfalse.netrc
.docker/config.jsonfalse.docker/config.json
.envfalse.env
./.envfalse./.env
.git-credentialsfalse.git-credentials
sub/.env (control)truenull
x/../.env (control)truenull
/Users/x/.ssh/id_rsa (control)truenull
/etc/passwd (control)truenull
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.

Impact

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.

Suggested fix

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');

Notes

  • Related, not claimed: a hostname that resolves to loopback (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.
  • Also not 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.
  • Existing regression tests 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.
  • These variants are not described in the two published advisories, and no public issue or pull request mentions them.

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

Related JavaScript vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-v25x-53wq-6r99HIGH8.2
  • JavaScript logoJavaScript
  • mcp-sanitizer
NoYesOct 31, 2026
GHSA-grx6-75c5-f738HIGH7.1
  • JavaScript logoJavaScript
  • compressing
NoYesOct 31, 2026
GHSA-qvp2-8q5w-gcfpMEDIUM6.5
  • JavaScript logoJavaScript
  • mcp-sanitizer
NoYesOct 31, 2026
GHSA-vh7f-3c46-hfp3LOWN/A
  • JavaScript logoJavaScript
  • cron-parser
NoNoOct 31, 2026
GHSA-f7wr-f2gj-v69xLOWN/A
  • JavaScript logoJavaScript
  • seroval
NoYesOct 31, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management