A bouncer checking IDs with the wrong alphabet while the doorman behind him reads perfectly
CVE-2026-16221 is a URI-parser interpretation conflict in fast-uri, the RFC 3986 toolbox maintained under the Fastify org. Versions 2.3.1–2.4.2, 3.0.0–3.1.3, and 4.0.0–4.1.0 do not treat a literal backslash (U+005C) as an authority delimiter. Node.js's WHATWG URL parser — the engine behind fetch(), undici, and the native http/https modules — normalizes \ to / for special schemes (http, https, ws, wss, ftp, file). The result: http://evil.com\@allowed.com resolves to host allowed.com in fast-uri but host evil.com in Node's parser. If an application uses fast-uri to enforce a host allowlist and then hands the same string to fetch(), the attacker bypasses the check and steers the request to an arbitrary destination — including cloud metadata endpoints like 169.254.169.254.
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 the vector math is technically correct *if you assume the app uses fast-uri as its SSRF policy gate*. That assumption dramatically overstates real-world exposure. fast-uri pulls ~90–120 million weekly npm downloads, but the overwhelming majority arrive as a transitive dependency of ajv (the JSON Schema validator), which uses fast-uri solely for $id / $ref schema resolution — a code path that never touches outbound HTTP allowlists. The subset of applications that (a) call fast-uri.parse() directly for host-policy enforcement, (b) then pass the identical string to Node's WHATWG parser, and (c) expose that endpoint to untrusted input is genuinely small. No exploitation in the wild, no indexed PoC, EPSS at the 16th percentile (0.25%), and not on CISA KEV. The vendor HIGH is fair for the narrow pattern that's actually vulnerable, but for fleet-wide prioritization it overstates urgency.
4 steps from start to impact.
Identify a fast-uri–gated SSRF surface
fast-uri.parse() to enforce a host allowlist or denylist, and then forwards the same raw string to Node's native URL consumer (fetch(), undici, http.request()). This is a specific architectural pattern, not a default behavior of any framework.- Target application uses fast-uri directly for URL policy enforcement
- User-supplied URL reaches the fast-uri parse path
- The vast majority of fast-uri installations are transitive through ajv for JSON Schema
$refresolution — they never touch HTTP request URLs - Fastify's own routing does not use fast-uri for security-gating outbound requests
- Most SSRF-protection libraries in the Node ecosystem (e.g., ssrf-req-filter, private-ip) use Node's native URL, not fast-uri
npm audit catches this automatically. No network-layer scanner can detect this — it is a code-level pattern.Craft a backslash-confused URL
http://evil.com\@allowed.com. fast-uri reads the backslash as part of the userinfo field, extracting host allowed.com. Node's WHATWG URL reads \ as a path separator after authority, extracting host evil.com. The payload is trivial — no encoding tricks, no race conditions, no heap gymnastics.- Attacker can supply an arbitrary URL string to the vulnerable endpoint
- Some WAFs and input-validation layers may strip or reject raw backslashes in URL parameters, though this is not universal
- URL-encoding the backslash (
%5C) may or may not reproduce the bug depending on whether the application decodes before parsing
Bypass allowlist / SSRF filter
allowed.com as the hostname. The application's allowlist check passes. The same raw URL string is then passed to fetch() or http.request(), where Node's parser resolves it to evil.com. The request leaves the application bound for the attacker's host or an internal target.- The application does not re-parse the URL with the same parser that the HTTP client uses
- The allowlist check relies exclusively on fast-uri's host extraction
- Applications that validate AND request using the same parser (e.g., both using
new URL()) are immune - Defense-in-depth SSRF controls at the network layer (blocking RFC 1918, link-local, metadata IPs at the firewall/proxy) still stop the most damaging outcomes
Reach internal target or cloud metadata
169.254.169.254 (cloud metadata), 127.0.0.1, or any internal host. On cloud infrastructure, a successful metadata read can yield IAM credentials, enabling lateral movement. On-prem, the attacker can scan internal services.- The application's IAM role or network position grants access to sensitive internal endpoints
- Cloud metadata endpoint is not blocked by IMDSv2 enforcement or network policy
- AWS IMDSv2 (token-required) blocks SSRF-driven metadata reads from simple GET requests
- GCP and Azure metadata endpoints require specific headers that the application's HTTP client may not set
- Network-level egress controls (security groups, NACLs, service mesh policies) can block lateral requests
npm audit fix or npm update fast-uri in every Node.js project. Since ajv's semver range (^3.0.1) already accepts the patched version, this is a lockfile-only change with zero code modifications. Recommend jumping to 3.1.8+ or 4.2.2 to also cover sibling CVEs (CVE-2026-18446 and later). As a MEDIUM verdict, there is no noisgate mitigation SLA — go straight to the 365-day remediation window, though the fix is trivial enough to deploy in days.169.254.169.254. GCP and Azure metadata endpoints require specific headers (Metadata-Flavor: Google, Metadata: true). This eliminates the highest-impact SSRF outcome (credential theft from metadata) even if the parser confusion is exploited. Deploy within existing cloud hardening cycles.169.254.169.254/32, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 unless explicitly required. This provides defense-in-depth against any SSRF bypass, not just this CVE.new URL(). Never parse with fast-uri for security decisions and then pass the raw string to fetch(). This architectural fix eliminates the entire class of parser-confusion SSRF.- WAF rules blocking backslashes in URL parameters — The backslash may arrive URL-encoded (
%5C), and many legitimate URLs contain backslashes in query strings or fragments. The false-positive rate makes this impractical at scale. - Upgrading ajv alone — ajv's package.json does not pin a specific fast-uri patch version; it uses
^3.0.1. The ajv package version is irrelevant — you must update the *resolved* fast-uri version in your lockfile. - Input sanitization / stripping backslashes — The vulnerability is in the parser, not in input handling. Stripping backslashes from URLs can break legitimate functionality and may be bypassed by double-encoding or alternate representations.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed on CISA KEV. No GreyNoise tags. No campaign reporting from Proofpoint, Mandiant, or CrowdStrike references this CVE. The vulnerability is library-level and not directly scannable from the network. |
|---|---|
| Proof-of-Concept Availability | No public PoC indexed. Checked pocindex.io, ExploitDB, Nuclei templates, and GitHub repos named after the CVE — no dedicated exploit code. The GHSA advisory includes the example payload http://evil.com\@allowed.com which is effectively a one-liner PoC for anyone who reads the advisory. Weaponization is trivial but target identification is hard. |
| EPSS Score | 0.00254 (0.25%) — 16th percentile. This places it well below the exploitation-likely threshold. The model reflects the absence of in-the-wild activity and PoC tooling. |
| CISA KEV Status | Not listed. No known active exploitation meets KEV inclusion criteria. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N — Network-accessible, low complexity, no privileges, no interaction, unchanged scope, high integrity impact, no confidentiality or availability impact. Snyk independently scored 8.7 using a slightly different assessment. The vector assumes the vulnerable code path is reachable, which is the key question for real deployments. |
| Affected Versions | fast-uri 2.3.1–2.4.2, 3.0.0–3.1.3, 4.0.0–4.1.0. The 3.x line is the most prevalent in the wild because ajv pins ^3.0.1. |
| Fixed Versions | 2.4.3, 3.1.4, 4.1.1. All are semver-compatible with ajv's ^3.0.1 constraint, so a lockfile refresh (npm update fast-uri or npm audit fix) is sufficient — no ajv version bump required. |
| Installed Base / Exposure | fast-uri has ~88–122 million weekly npm downloads and 323 direct dependents on npm. The primary consumer is ajv (JSON Schema validator), which itself has 200M+ weekly downloads. However, ajv uses fast-uri for $id/$ref schema resolution, not for HTTP URL policy enforcement. The *reachable vulnerable surface* is a small fraction of the installed base. |
| Disclosure & Credit | Disclosed 2026-07-19 by researcher rampage0010. Remediation reviewed by mcollina (Matteo Collina, Fastify lead) and UlisesGascon. Published as GHSA-v2hh-gcrm-f6hx. |
| Related CVEs | CVE-2026-18446 (GHSA for backslash *authority introducer* variant, fixed in 2.4.4/3.1.5/4.1.2) is a sibling bug in the same parser. Teams patching CVE-2026-16221 should jump straight to 3.1.8+ or 4.2.2 to cover all known fast-uri parsing issues. |
Sources.
Why this verdict
- Reachability gap is the dominant factor. The CVSS vector assumes an attacker can reach the fast-uri→WHATWG desync code path. In practice, ~95%+ of the 88–122M weekly installs arrive via ajv for JSON Schema
$refresolution — a path that never gates outbound HTTP requests. The number of applications callingfast-uri.parse()directly for SSRF/allowlist enforcement is vanishingly small. - No exploitation signal compounds the reachability narrowing. Zero in-the-wild exploitation, no KEV listing, EPSS at 16th percentile (0.25%), and no weaponized PoC tooling indexed on pocindex.io or GitHub. The advisory's example payload is trivial to reproduce but requires a target with the specific dual-parser pattern.
- Network-layer SSRF defenses cap the blast radius. Even where the vulnerable code path exists, AWS IMDSv2, GCP/Azure metadata header requirements, egress security groups, and network-level RFC 1918 blocking independently prevent the most damaging SSRF outcomes. These controls are standard in any enterprise cloud deployment.
- Role multiplier: fast-uri is a utility library, not infrastructure. It does not run on domain controllers, hypervisors, CI/CD runners, or network edge appliances. It runs inside Node.js application processes. (a) *Low-value role*: dev tooling, build scripts, test suites — the schema-validation path is not exploitable. (b) *Typical role*: line-of-business Node.js API server using ajv for request validation — fast-uri's
$refresolution is not in the HTTP-policy hot path; chain does not succeed. (c) *High-value role*: a Node.js API gateway or proxy that uses fast-uri directly to validate outbound URLs before forwarding — the chain succeeds and SSRF could reach cloud metadata or internal services. However, this represents a tiny fraction (<1%) of the fast-uri installed base. The blast radius is application-scoped (single service's IAM role), not domain/fleet/supply-chain scoped. This does not meet the HIGH floor threshold because the affected component is not canonically a high-value-role component, and the high-value deployment pattern represents well under 1% of installs. - The fix is frictionless.
npm audit fixornpm update fast-uriresolves the issue without breaking changes. ajv's^3.0.1constraint already accepts the patched 3.1.4+. This is a lockfile-only remediation.
Why not higher?
The vendor's HIGH 7.5 would be appropriate if fast-uri were commonly used as the SSRF policy gate in production Node.js services. It is not — it is overwhelmingly consumed as a transitive dependency of ajv for JSON Schema validation, a code path that is not exploitable via this CVE. The absence of any exploitation evidence, any indexed PoC tooling, and any KEV listing after three months of disclosure reinforces that the theoretical attack surface has not translated to practical risk. The high-value role (API gateway using fast-uri for URL policy) represents far less than 1% of the installed base, keeping this below the HIGH floor.
Why not lower?
The attack payload is trivially simple (http://evil.com\@allowed.com), requires no authentication or user interaction, and the advisory text itself serves as a de facto PoC. For the minority of applications that DO use fast-uri for URL policy enforcement, exploitation is straightforward and the SSRF impact is real. The massive installed base (88M+ weekly downloads) means even a small percentage of vulnerable-pattern deployments could represent thousands of applications. Dropping to LOW would understate the risk for those specific deployments.
Crowdsourced verification payload.
Run this script from the root of any Node.js project (where package.json lives) on the auditor's workstation or in CI. Requires node (v14+) on PATH. No elevated privileges needed. Invoke: bash check_cve_2026_16221.sh /path/to/project
#!/usr/bin/env bash
# check_cve_2026_16221.sh — Detect vulnerable fast-uri (CVE-2026-16221)
# Usage: bash check_cve_2026_16221.sh [project_dir]
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
PROJDIR="${1:-.}"
if [ ! -d "$PROJDIR/node_modules" ]; then
echo "[UNKNOWN] No node_modules found in $PROJDIR — run npm install first or specify the project root."
exit 2
fi
# Find all installed fast-uri versions
FOUND=0
VULN=0
while IFS= read -r pkgjson; do
VERSION=$(node -e "try{console.log(require('$pkgjson').version)}catch(e){console.log('err')}" 2>/dev/null)
if [ "$VERSION" = "err" ] || [ -z "$VERSION" ]; then
continue
fi
FOUND=1
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3 | cut -d- -f1)
IS_VULN=0
case "$MAJOR" in
2)
# Vulnerable: 2.3.1 through 2.4.2
if [ "$MINOR" -eq 3 ] && [ "$PATCH" -ge 1 ]; then IS_VULN=1; fi
if [ "$MINOR" -eq 4 ] && [ "$PATCH" -le 2 ]; then IS_VULN=1; fi
;;
3)
# Vulnerable: 3.0.0 through 3.1.3
if [ "$MINOR" -eq 0 ]; then IS_VULN=1; fi
if [ "$MINOR" -eq 1 ] && [ "$PATCH" -le 3 ]; then IS_VULN=1; fi
;;
4)
# Vulnerable: 4.0.0 through 4.1.0
if [ "$MINOR" -eq 0 ]; then IS_VULN=1; fi
if [ "$MINOR" -eq 1 ] && [ "$PATCH" -eq 0 ]; then IS_VULN=1; fi
;;
esac
if [ "$IS_VULN" -eq 1 ]; then
echo "[VULNERABLE] fast-uri $VERSION found at: $(dirname "$pkgjson")"
VULN=1
else
echo "[PATCHED] fast-uri $VERSION found at: $(dirname "$pkgjson")"
fi
done < <(find "$PROJDIR/node_modules" -path '*/fast-uri/package.json' -not -path '*/node_modules/*/node_modules/*/node_modules/*' 2>/dev/null)
if [ "$FOUND" -eq 0 ]; then
echo "[UNKNOWN] fast-uri not found in $PROJDIR/node_modules — package may not be a dependency."
exit 2
fi
if [ "$VULN" -eq 1 ]; then
echo ""
echo "Remediation: run 'npm update fast-uri' or 'npm audit fix' to pull >=3.1.4"
exit 1
else
echo ""
echo "All fast-uri instances are patched."
exit 0
fi