← Back to Feed CACHED · 2026-09-15 11:03:13 · CACHE_KEY CVE-2026-86472
CVE-2026-86472 · CWE-178 · Disclosed 2026-09-15

fast-uri inconsistent host case normalization via percent-encoded octets

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

It's like a bouncer who checks the guest list before putting on his reading glasses

CVE-2026-86472 affects the fast-uri npm package (versions < 2.4.7, 3.0.0–3.1.7, 4.0.0–4.1.4), a dependency-free RFC 3986 URI toolbox with ~100 million weekly downloads. The bug is a sequencing error: normalize() folds the hostname to lowercase *before* percent-decoding, so %41 (which should decode to a) survives as uppercase A. The result is that parse('//%41.com').host returns A.com while parse('//a.com').host returns a.com. Applications performing case-sensitive host checks against parsed output can reach inconsistent conclusions about the same effective destination.

The vendor's MEDIUM / 4.8 is already modest, but it still overstates the practical risk. The CVSS vector correctly sets AC:H (high attack complexity), yet the real friction is deeper than complexity alone. Over 99% of fast-uri's install base consumes it transitively through AJV and Fastify for JSON Schema $ref resolution — not for security-relevant URL validation. An attacker must find an application that (a) directly uses fast-uri's host output for access-control decisions, (b) performs that comparison case-sensitively, and (c) accepts attacker-controlled URLs. That three-gate chain makes exploitation a niche scenario, not a fleet-wide concern.

"Library-level case-folding ordering bug; real-world exploitability needs a rare usage pattern most apps never hit."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Identify target application using fast-uri for host validation

The attacker must find a Node.js service that calls fast-uri.parse() or fast-uri.normalize() on untrusted URLs and then checks the parsed .host property against a denylist or allowlist. This is an uncommon pattern — most fast-uri consumers inherit it through AJV for schema validation, not URL policy enforcement.
Conditions required:
  • Target app uses fast-uri directly (not just transitively via AJV)
  • App uses parsed host for security-critical decisions
Where this breaks in practice:
  • Vast majority of fast-uri installs are transitive AJV/Fastify deps
  • Application must expose a URL input surface to attacker
STEP 02

Craft URL with percent-encoded hostname octets

The attacker constructs a URL where hostname characters are percent-encoded, e.g. http://%49nternal.corp.com/secret. Because fast-uri lowercases *before* decoding, the parsed host becomes Internal.corp.com instead of internal.corp.com. No tooling beyond a browser or curl is needed.
Conditions required:
  • Knowledge of the target's denylist/allowlist entries
  • Ability to submit crafted URLs to the application
Where this breaks in practice:
  • DNS is case-insensitive, so the resolved IP is identical regardless of casing
  • Many apps use .toLowerCase() on host before comparison, negating the bug
STEP 03

Bypass case-sensitive host check

If the app performs a strict equality check like parsed.host === 'internal.corp.com', the percent-encoded variant yields Internal.corp.com which does not match. For a denylist, this means the blocked host is reachable; for an allowlist, the legitimate host is rejected (DoS, not escalation). The attacker's interesting path is denylist bypass leading to SSRF.
Conditions required:
  • Host comparison must be case-sensitive (===, not case-folded)
  • Denylist scenario for offensive value
Where this breaks in practice:
  • Downstream HTTP clients (Node fetch, axios) resolve DNS case-insensitively — the actual connection target is unchanged
  • Modern SSRF frameworks typically validate at the IP/socket level, not hostname string level
Detection/coverage: WAFs and SSRF filters operating on raw request URLs (not parsed host) would see the percent-encoded form and may flag it.
STEP 04

Reach internal resource via SSRF

If the denylist bypass succeeds and the downstream HTTP client follows the URL, the attacker reaches the nominally blocked internal endpoint. Impact is bounded by what that endpoint exposes — typically metadata services, internal APIs, or cloud instance credentials.
Conditions required:
  • Vulnerable app proxies or fetches the attacker-supplied URL server-side
  • Internal target has exploitable resources
Where this breaks in practice:
  • Cloud metadata services (169.254.169.254) are IP-based, not hostname-based — this bug doesn't help
  • Most SSRF-hardened apps validate resolved IPs, not just hostnames
Detection/coverage: Cloud provider metadata endpoint access logs; network-level SSRF detection in WAF/CSPM.
03 · Compensating Control

1
Lowercase parsed hosts before comparison — Add .toLowerCase() to any code path that compares fast-uri.parse().host against allow/deny lists. This neutralizes the bug entirely and takes minutes to deploy. No deadline pressure — this is a LOW finding, so treat as backlog hygiene.
2
Validate at the IP/socket layer for SSRF — Instead of comparing hostname strings, resolve the URL to an IP address and validate against blocked CIDR ranges (e.g., 169.254.0.0/16, 10.0.0.0/8, 127.0.0.0/8). This is the correct SSRF defense regardless of this CVE.
3
Upgrade fast-uri to patched version — Run npm audit fix or pin fast-uri to >=2.4.7 / >=3.1.8 / >=4.1.5. Since this is a LOW verdict, fold into your next scheduled dependency update cycle within 365 days per the noisgate remediation SLA.
What doesn't work
  • WAF URL-decoding rules — Most WAFs decode percent-encoding in paths and query strings but do not normalize hostname casing. The bypass operates at the hostname level before the WAF's purview.
  • Content Security Policy (CSP) — CSP is a browser-side control; this vulnerability is server-side URL parsing. CSP headers have no effect on server-side host validation logic.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed. Not listed in CISA KEV. No known campaigns or threat actor usage.
Proof of conceptAdvisory includes a trivial one-liner: parse('//%41.com').host → A.com. No weaponized PoC or exploit tool exists.
EPSS scoreNot yet scored (published 2026-09-15). Expected to land in the bottom quartile given AC:H and library-level impact.
KEV statusNot listed. No federal mandate to patch.
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N — network-reachable but high complexity, low confidentiality and integrity impact, no availability impact.
Affected versionsfast-uri < 2.4.7, 3.0.0–3.1.7, 4.0.0–4.1.4
Fixed versions2.4.7, 3.1.8, 4.1.5
Exposure / install base~100M weekly npm downloads, but 303 direct dependents. Overwhelmingly consumed transitively via AJV and Fastify for schema validation, not URL security.
Disclosure date2026-09-15 (coordinated disclosure via GitHub Security Advisory GHSA-hrr3-gc8f-f4qj)
Reporter / maintainerDisclosed via Fastify project; patch authored by Matteo Collina (@mcollina).

Sources.

  1. GitHub Security Advisory GHSA-hrr3-gc8f-f4qj
  2. fast-uri v3.1.8 release notes
  3. fast-uri on npm
  4. GitLab Advisory Database - fast-uri
  5. Fastify ajv-compiler fast-uri integration
  6. fast-uri on Snyk
05 · The Call

Final Verdict
↓ DOWNGRADED to LOW (3.0/10)

Why this verdict

  • Transitive-only exposure for 99%+ of installs: fast-uri's 100M weekly downloads are driven by AJV and Fastify, which use it for $ref resolution in JSON Schema — not for URL allowlist/denylist enforcement. The bug is unreachable in this dominant usage path.
  • Three-gate exploitation chain: Attacker needs (1) an app calling fast-uri.parse() on user input, (2) case-sensitive host comparison for access control, (3) server-side fetch of the resulting URL. Each gate eliminates most of the remaining population.
  • DNS case-insensitivity neutralizes impact: Even when the parsed host string differs in casing, DNS resolution is case-insensitive. The downstream HTTP client connects to the same IP regardless, meaning the bug only matters when the *string comparison* is the security boundary — not the network connection.
  • Role multiplier: fast-uri is a utility library, not a component that defines a deployment role. It appears in CI/CD (build-time via Webpack/schema-utils), in API servers (Fastify), and in validation layers (AJV). In none of these canonical roles does the host-casing bug grant domain takeover, fleet compromise, or supply-chain pivot. The blast radius is limited to the individual application's URL-validation logic — host-level, not fleet-level. No high-value role floor applies.
  • No exploitation evidence, no weaponized tooling: Zero in-the-wild activity, no KEV listing, no researcher PoC beyond the advisory's one-liner example.

Why not higher?

Upgrading to MEDIUM or above would require either active exploitation evidence or a broader blast radius. The bug does not enable RCE, privilege escalation, or data exfiltration on its own — it is a logic inconsistency that *might* weaken a host check in a bespoke application. The vendor's own AC:H rating already acknowledges the difficulty, and the real-world narrowing (transitive-only usage, DNS case-insensitivity) compounds further.

Why not lower?

A full IGNORE verdict would be inappropriate because the bug *is* real and *could* weaken SSRF protections in the small population of apps that directly use fast-uri for URL policy. The advisory example is trivially reproducible, the fix is a one-line npm update, and defenders should still track it for completeness.

06 · Verification

Crowdsourced verification payload.

Run on any machine with Node.js installed. Execute: node /tmp/check-fast-uri-cve-2026-86472.js. No special privileges required.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env node
// CVE-2026-86472 - fast-uri inconsistent host case normalization
// Outputs: VULNERABLE / PATCHED / UNKNOWN

try {
  const fastUri = require('fast-uri');
  const pkg = require('fast-uri/package.json');
  const version = pkg.version;

  // Test: parse a URL with percent-encoded uppercase octet in host
  const result = fastUri.parse('//%41.example.com');
  const host = result.host || '';

  // In patched versions, host should be lowercased to 'a.example.com'
  // In vulnerable versions, host will be 'A.example.com'
  if (host === 'A.example.com' || host === '%41.example.com') {
    console.log('VULNERABLE');
    console.log('fast-uri version: ' + version);
    console.log('Parsed host: ' + host + ' (expected: a.example.com)');
    process.exit(1);
  } else if (host.toLowerCase() === 'a.example.com') {
    console.log('PATCHED');
    console.log('fast-uri version: ' + version);
    console.log('Parsed host: ' + host);
    process.exit(0);
  } else {
    console.log('UNKNOWN');
    console.log('fast-uri version: ' + version);
    console.log('Unexpected host value: ' + host);
    process.exit(2);
  }
} catch (e) {
  if (e.code === 'MODULE_NOT_FOUND') {
    console.log('UNKNOWN');
    console.log('fast-uri is not installed in this project.');
    process.exit(2);
  }
  console.log('UNKNOWN');
  console.log('Error: ' + e.message);
  process.exit(2);
}