A bouncer who checks IDs with one alphabet while the club uses another
CVE-2026-18446 is a URI-parser interpretation conflict in fast-uri, the dependency-free RFC 3986 toolbox maintained under the Fastify org. Versions before 2.4.4, 3.0.0–3.1.4, and 4.0.0–4.1.1 require a literal // to recognize a URI authority. A reference using \\, /\, or \/ as the authority introducer is parsed with no authority at all — the introducer and everything after it fold into the path component. Node's native WHATWG URL parser, however, treats backslash as interchangeable with forward slash for special schemes (http, https, ws, wss, ftp, file). The result: fast-uri sees http:\\/evil.com/path as a path-only reference, while new URL() and fetch() resolve it to host evil.com. Applications that gate on fast-uri's host extraction before handing the same raw string to Node's HTTP stack have a classic parser-desync SSRF or redirect-bypass.
The vendor rates this HIGH 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N), and *if you squint at just the parser bug* that's defensible — it's unauthenticated, network-reachable, and breaks integrity of host-based policy. But the rating ignores the elephant in the room: fast-uri pulls ~100 million weekly npm downloads, and the overwhelming majority are transitive through Ajv (the JSON Schema validator), where it resolves $id and $ref URIs defined by schema authors — not by untrusted user input. The vulnerable code path — 'application uses fast-uri to enforce an allowlist, then passes the same raw string to fetch()' — is a niche, bespoke pattern. The CVSS assumes that path is always reachable; in practice it almost never is. This is a textbook case of a high base score that dramatically overstates fleet-wide risk.
4 steps from start to impact.
Attacker crafts a backslash-introducer URL
http:\\/evil.com\@allowed.com/callback or http:\/\internal-metadata/latest/meta-data/. The backslash variants (\\, \/, /\) are syntactically unusual but pass through most input channels (query params, JSON payloads, form fields) without modification. No tooling is required — this is a hand-crafted string.- Attacker can submit a URL string to the target application
- The application must actually accept arbitrary URLs as input, not just path fragments or structured parameters
Application validates host via fast-uri
parse() or resolve() on the attacker's URL to extract the host component. Because fast-uri requires literal // for authority recognition, it returns no host (or the wrong host when @ is involved). The URL passes the application's allowlist, denylist, or loopback check. This step is the crux: it only fires if the developer chose fast-uri — not Node's URL, not url.parse(), not a WAF — as the trust boundary for host extraction.- Application uses fast-uri (not Node URL) for security-critical host validation
- The validation result gates a subsequent HTTP request
- The vast majority of fast-uri installations are via Ajv for JSON Schema
$refresolution, where user input never reaches the parser - Developers building SSRF protection typically use
new URL()or dedicated libraries likessrf-req-filter, not a low-level URI toolkit - No major framework (Express, Fastify, Next.js, Koa) uses fast-uri for request-URL host validation in its core
require('fast-uri').parse() calls in security-sensitive code pathsApplication passes raw string to Node's HTTP stack
fetch(), http.request(), or new URL() for the actual outbound request. Node's WHATWG URL parser normalizes the backslash to a forward slash, resolving evil.com (or 169.254.169.254, or a loopback address) as the actual host. The request goes to the attacker-controlled or internal destination.- The raw URL string (not a re-serialized version) is passed to the HTTP client
- The HTTP client uses WHATWG URL parsing (Node 18+
fetch,undici,new URL())
- If the application re-serializes via
fast-uri.serialize()after parsing, the authority is gone and the HTTP client receives a path-only reference that fails or routes locally — the SSRF doesn't land - Legacy
url.parse()(deprecated but still common) has different backslash handling that may not produce the same desync
SSRF or redirect bypass achieved
- The server has network access to the attacker's target (internal service, metadata endpoint, etc.)
- Cloud providers increasingly require IMDSv2 (token-based) which blocks blind SSRF to metadata endpoints
- Network segmentation and egress filtering limit what the server can reach
- SSRF-specific mitigations (metadata endpoint blocking, private IP denylist at the HTTP client layer) are common in mature environments
npm audit fix or explicitly pin fast-uri to ≥ 2.4.4 / ≥ 3.1.5 / ≥ 4.1.2 (or better, ≥ 2.4.7 / ≥ 3.1.8 / ≥ 4.1.5 to cover the full cluster of host-confusion CVEs). For lockfile-managed monorepos, use npm overrides or yarn resolutions to force the transitive version. Since this is a MEDIUM verdict, there is no noisgate mitigation SLA — go straight to the 365-day remediation window per the noisgate remediation SLA, but given the trivial upgrade path, aim for your next scheduled dependency refresh cycle.require('fast-uri') or import.*from.*fast-uri in application code (not node_modules). If fast-uri is only present as a transitive Ajv dependency and never imported directly, the vulnerability is not reachable in your application and no further action is needed beyond eventual version bumping. If you DO find direct imports used for URL validation or SSRF filtering, treat those instances as HIGH priority and patch immediately.new URL(input) for host extraction — it uses the same WHATWG parser as fetch() and undici, eliminating the parser-desync class entirely. fast-uri should be used for RFC 3986 operations (resolve, normalize, compare), not as a security boundary for host allowlisting.- WAF backslash-in-URL rules — Backslashes in URLs are syntactically valid in many contexts (Windows UNC paths, some legacy applications). Blocking all backslash-containing URLs at the WAF would cause false positives without addressing the root cause, which is parser disagreement inside the application.
- Upgrading Node.js — The desync is between fast-uri's RFC 3986 parser and Node's WHATWG URL parser. Both parsers are behaving according to their respective specs; the bug is in fast-uri's failure to handle the backslash-as-slash normalization that WHATWG mandates. Upgrading Node doesn't change fast-uri's behavior.
- npm audit alone without resolution enforcement —
npm auditwill flag the CVE, but many teams have hundreds of audit findings and no automated path to force transitive dependency upgrades. Withoutoverrides/resolutionsor a lockfile update, the vulnerable version persists.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed on CISA KEV. No GreyNoise tags or Shodan honeypot hits specific to fast-uri backslash confusion. General SSRF scanning campaigns are active (GreyNoise tracked 400+ IPs exploiting SSRF flaws in 2025–2026) but none tied to this CVE. |
|---|---|
| Proof-of-Concept | Trivially constructable but no weaponized exploit repo found. The fix commit (2d50fba) includes nine test vectors (e.g., http://evil.com\@allowed.com). No dedicated PoC repo on GitHub, no pocindex.io listing, no Nuclei template, no ExploitDB entry. The bug is simple enough that any developer can reproduce it in 5 lines of Node.js, but weaponization requires a vulnerable application, not just the library. |
| EPSS | 0.00221 (0.221% probability of exploitation in the next 30 days). This is low — roughly 55th–65th percentile, consistent with a library-level parser bug that requires a specific application architecture to exploit. |
| CISA KEV | Not listed. No known dates for addition. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N — Network-reachable, low complexity, no auth, no user interaction, high integrity impact, no confidentiality or availability impact. The vector correctly models the parser bug in isolation but does not account for the narrow reachability of the vulnerable code path in real deployments. |
| Affected Versions | All of: < 2.4.4, 3.0.0–3.1.4, 4.0.0–4.1.1. Fixed in 2.4.4, 3.1.5, 4.1.2. Note: fast-uri has had a cluster of similar host-confusion CVEs (CVE-2026-16221, CVE-2026-6322, CVE-2026-84394, CVE-2026-75931, CVE-2026-75899, CVE-2026-75975, CVE-2026-86472) — if you're patching, jump to ≥ 2.4.7 / ≥ 3.1.8 / ≥ 4.1.5 to cover them all. |
| Install Base & Ecosystem | ~88–122M weekly npm downloads. 323 direct dependents. Primary consumers are Ajv (JSON Schema validator, ~70M weekly downloads) and Fastify (web framework). In the Ajv path, fast-uri resolves $id/$ref schema URIs — these are developer-defined, not user-controlled, so the vulnerability is not reachable in this dominant usage pattern. |
| Disclosure Timeline | Reported by iaohkut from NightWolf Team. GHSA published 2026-07-31. GitLab advisory dated 2026-08-03. Fix commits landed in the same release cycle. The researcher has reported multiple fast-uri host-confusion variants and vulnerabilities in other OSS projects (Browserslist, better-auth, Tornado). |
| Scanning / Exposure | Not directly measurable. fast-uri is a library, not a network service — Shodan/Censys cannot fingerprint it. Exposure depends entirely on whether a given Node.js application uses fast-uri for security-critical URL validation. Software composition analysis (SCA) tools (Snyk, Socket.dev, npm audit) flag the vulnerable version in dependency trees. |
| Reporter | iaohkut (NightWolf Team) — prolific OSS security researcher with multiple 2026 advisories across fast-uri, Browserslist, better-auth, and Tornado. |
Sources.
Why this verdict
- Dominant usage path is not vulnerable. >99% of fast-uri's ~100M weekly installs are transitive via Ajv, where it resolves schema
$refURIs defined by developers — never by untrusted user input. The parser-desync bug is unreachable in this path. The CVSS AV:N/AC:L/PR:N model assumes the vulnerable parse call is directly attacker-accessible; in the Ajv path, it is not. - Exploitation requires a bespoke application pattern. The attacker needs an app that (a) imports fast-uri directly, (b) uses it to extract and validate a host from user-supplied URLs, and (c) then passes the same raw string to Node's
URL/fetch. This three-link chain is uncommon — security-conscious Node.js developers typically usenew URL()or SSRF-specific libraries for host gating, not a URI toolkit. - No in-the-wild exploitation and low EPSS. EPSS 0.00221, no KEV listing, no GreyNoise/Shodan signals, no weaponized PoC. The bug is trivial to demonstrate but requires a vulnerable application to exploit — it's a class break in URI parsing, not a point-and-shoot exploit.
- Role multiplier: fast-uri is a utility library, not infrastructure. It is not a hypervisor, IdP, PAM vault, CA, kernel agent, network edge appliance, or domain controller. It runs wherever Node.js runs, but its blast radius when exploited is bounded to host-level SSRF — access to what the server process can reach on the network. There is no inherent path to domain takeover, fleet compromise, or supply-chain pivot from this bug alone. Even in Fastify-based API servers (the highest-value realistic role), Fastify's core does not use fast-uri for request-URL host validation — it uses it only through Ajv for schema validation. A developer would have to explicitly wire fast-uri into a custom SSRF guard, placing this in the <1% tail of the installed base.
- Impact ceiling is integrity-only SSRF. The CVSS correctly notes C:N/A:N — this is not data exfiltration or RCE. The worst realistic outcome is SSRF to internal services or cloud metadata, which is serious but host-scoped and increasingly mitigated by IMDSv2, network segmentation, and egress filtering.
Why not higher?
The vendor's HIGH 7.5 would be appropriate if fast-uri were commonly used as a security gate for URL validation, but it isn't — it's a schema-resolution utility that happens to expose a URI parser. The attack surface that matters (direct fast-uri host validation before fetch) is a tiny fraction of the install base. No exploitation evidence, no weaponized tooling, and EPSS in the bottom half of HIGH-rated CVEs all point away from HIGH. The role multiplier doesn't trigger a HIGH floor because fast-uri is not canonically a high-value-role component, and the vulnerable pattern represents <1% of installs.
Why not lower?
Despite the narrow reachability, this IS a real parser-desync vulnerability that can bypass SSRF protections in applications that do use fast-uri for host validation. The bug is trivially reproducible, requires no authentication, and the fix commit's test vectors are public — any attacker who identifies a target app using this pattern can exploit it with zero tooling. The sheer size of the npm install base means even <1% represents thousands of projects, some of which may be security-sensitive. A LOW rating would understate the risk for the minority of installations where the bug is reachable.
Crowdsourced verification payload.
Run this on any host or CI runner where a Node.js project's node_modules is installed. Execute from the project root: bash check_cve_2026_18446.sh. Requires npm on PATH. No elevated privileges needed.
#!/usr/bin/env bash
# check_cve_2026_18446.sh — Detect fast-uri versions vulnerable to CVE-2026-18446
# Host confusion via backslash authority introducer
# Fixed in: 2.4.4, 3.1.5, 4.1.2
# Exit codes: 0=PATCHED 1=VULNERABLE 2=UNKNOWN
set -uo pipefail
VULN=0; FOUND=0
if ! command -v npm &>/dev/null; then
echo "UNKNOWN — npm not found on PATH"
exit 2
fi
while IFS= read -r line; do
ver=$(echo "$line" | grep -oP 'fast-uri@\K[0-9]+\.[0-9]+\.[0-9]+' 2>/dev/null || true)
[ -z "$ver" ] && continue
FOUND=1
major=${ver%%.*}; rest=${ver#*.}; minor=${rest%%.*}; patch=${rest#*.}
safe=0
if [ "$major" -ge 5 ]; then safe=1
elif [ "$major" -eq 4 ]; then
[ "$minor" -gt 1 ] && safe=1
[ "$minor" -eq 1 ] && [ "$patch" -ge 2 ] && safe=1
elif [ "$major" -eq 3 ]; then
[ "$minor" -gt 1 ] && safe=1
[ "$minor" -eq 1 ] && [ "$patch" -ge 5 ] && safe=1
elif [ "$major" -eq 2 ]; then
[ "$minor" -gt 4 ] && safe=1
[ "$minor" -eq 4 ] && [ "$patch" -ge 4 ] && safe=1
fi
if [ "$safe" -eq 0 ]; then
echo "VULNERABLE — fast-uri@${ver} (CVE-2026-18446)"
VULN=1
else
echo "PATCHED — fast-uri@${ver}"
fi
done < <(npm ls fast-uri --all 2>/dev/null || true)
if [ "$FOUND" -eq 0 ]; then
echo "UNKNOWN — fast-uri not found in this project's dependency tree"
exit 2
fi
[ "$VULN" -eq 1 ] && exit 1
echo "All fast-uri instances patched."
exit 0