Four CVEs, One Bug: fast-uri and the Parser Differential Problem
The fast-uri package has now accumulated four separate host-confusion advisories. The most recent, CVE-2026-18446 (High, CVSS 7.5, published 31 July 2026), is patched in 2.4.4, 3.1.5 and 4.1.2. If you run Node and have ever validated a URL before fetching it, this advisory family is worth ten minutes of your attention — not because of one library, but because of the mistake underneath all four.
That mistake is a parser differential: two pieces of code read the same string and disagree about what it means. One of them makes the security decision. The other one makes the request.
What host confusion actually looks like
The clearest example comes straight from the CVE-2026-18446 advisory. Resolve the reference \\evil.com/path against the base https://allowed.com/:
- fast-uri produces
https://allowed.com/%5C%5Cevil.com/path— hostallowed.com, backslashes percent-encoded into the path. - Node's WHATWG
URLproduceshttps://evil.com/path— hostevil.com.
Both are defensible readings of a messy input. RFC 3986 says an authority begins with a literal //, and fast-uri implemented exactly that. The WHATWG URL Standard, which browsers and Node's fetch(), undici and http/https clients follow, treats \ as interchangeable with / for the special schemes: http, https, ws, wss, ftp and file. Neither parser is broken in isolation. The vulnerability lives in the gap between them.
Now put that gap inside a real application. A server validates a user-supplied URL with fast-uri, sees allowed.com, and approves the request. It then hands the original string to fetch(), which resolves it to evil.com and connects there. The allow-list was enforced against one destination and the request went to another. That is server-side request forgery through nothing more exotic than a punctuation disagreement.
The same bug, four times
What makes this family instructive is that it kept coming back through different syntax. Each advisory is a different way of writing the same idea:
- CVE-2026-13676 — failed IDN canonicalization.
http://127。0。0。1/uses ideographic full stops. fast-uri kept them literal; WHATWG andfetch()canonicalize them to127.0.0.1. A loopback denylist checks a hostname that never gets used. - CVE-2026-16221 — literal backslash authority delimiter. A single
\where the parsers disagree about whether the authority has ended. - GHSA-v39h-62p7-jpjc — percent-encoded authority delimiters. The same trick, hidden one encoding layer down.
- CVE-2026-18446 — backslash authority introducer.
\\,/\and\/as substitutes for//.
Four patches, four correct fixes, one unchanged root cause: a library that implements RFC 3986 is being used to gate a runtime that implements WHATWG. Patch four and a fifth encoding will eventually turn up. The durable fix is structural, and we will get to it below.
Why a build-only dependency still counts
Here is the part worth sitting with. In our own dependency tree, fast-uri is nowhere near production code. It arrives four levels down a build-tool chain — a PWA plugin, which pulls a service-worker builder, which pulls a JSON schema validator, which pulls fast-uri — and it is never shipped to a browser. It is, by the usual triage shorthand, “just a devDependency.”
That shorthand is worth retiring. A build-time dependency runs with your build machine's privileges, reads your source tree, and produces the artifact your users execute. The reason a dev-only URL-parsing flaw is lower risk than a production one is not that it cannot matter — it is that reaching it requires an attacker to already influence what your build feeds the parser. That is a real reduction in exposure. It is not zero, and it is not a reason to leave a High advisory sitting in the tree when the upgrade is a one-line lockfile change with no API surface.
We took the patch the day it surfaced. The interesting part was not the fix. It was how we found out.
A gate that cannot run is not a gate
Our CI pipeline runs a dependency-audit step that fails the build on any unapproved high or critical advisory, with a narrow, expiring exception file for advisories that have been reviewed and shown not to apply. It is a good control. It was also, at the moment this advisory landed, not running — CI had been rejected upstream for an unrelated billing reason, before any runner started.
So the gate was green in the sense that mattered least: nothing was reporting failure. No red X, no alert, no signal at all. The finding surfaced only because a human ran the audit by hand during a scheduled review.
This is the failure mode we now watch for above all others, because it is the one that looks exactly like success:
- A check that is disabled reports nothing, and nothing reads as fine.
- A check that never starts produces no failure to alert on.
- A check that runs against the wrong target passes honestly and proves nothing.
The lesson generalizes past dependency audits. For every automated control you rely on, you need a second signal that answers a different question: not “did it pass?” but “did it run at all, recently, against the thing I think it ran against?” A dead-man alarm on the pipeline itself. A last-success timestamp you can look at. A deliberate failure injected on a schedule to confirm the alarm still fires. Absence of a failure signal is not evidence of health.
Finding parser differentials in your own stack
The fast-uri family is a specific instance of a pattern you can go looking for deliberately. Ask one question of every security check in your codebase: is the component that validates this value the same component that later acts on it? Wherever the answer is no, you have a candidate.
- Find the validate/use pairs. Grep for URL allow-lists, redirect validators, SSRF filters, webhook destination checks and outbound-proxy routing. For each, identify which library parses during validation and which one performs the request.
- Validate the parsed object, not the string. This is the structural fix. Parse the input once with the same parser that will act on it — in Node, the WHATWG
URL— then run your policy against the resultingurl.hostname, and pass the reconstructedurlobject onward. Never re-parse the original string downstream. A differential needs two parsers; give it one. - Resolve DNS before you trust the host. Host-based allow-lists are weak on their own:
allowed.comcan resolve to169.254.169.254, and a redirect can move the request after the check. Validate the resolved IP, re-validate on every redirect hop, and prefer an egress proxy that enforces the policy at the network layer, where string parsing cannot reach it. - Fuzz the boundary. Feed both parsers the same corpus — backslashes, mixed slashes, percent-encoded delimiters, Unicode look-alike dots, userinfo
@tricks, embedded newlines, uppercase schemes — and assert that the extracted host matches. Any input where they disagree is a finding, with or without a CVE. - Look past URLs. The same shape appears in JSON parsers disagreeing about duplicate keys, HTTP servers and proxies disagreeing about
Content-LengthversusTransfer-Encoding(request smuggling), path normalizers disagreeing about..and encoded separators, and content-type checks disagreeing with the browser about what a MIME string means — a case we wrote about separately in the upload-filter bypass hiding in a charset parameter.
What to do this week
- Run
npm auditby hand, today, on every repository. Not in CI — by hand, so you learn whether CI has been telling you the truth. - If fast-uri appears anywhere in your tree, upgrade to at least 2.4.4, 3.1.5 or 4.1.2 depending on your major. Transitive copies count; check the lockfile, not the manifest.
- Confirm your audit gate ran in the last 24 hours, and can prove it.
- Pick your single most important URL allow-list and make it validate a parsed object rather than a string.
None of these is difficult. The fourth one is the only permanent fix, and it is the one that survives the fifth advisory in this family — which, on the evidence of the first four, is a question of when rather than whether.
Sources
- GitHub Advisory Database: CVE-2026-18446 — host confusion via backslash authority introducer
- GitHub Advisory Database: CVE-2026-16221 — host confusion via literal backslash authority delimiter
- GitHub Advisory Database: CVE-2026-13676 — host confusion via failed IDN canonicalization
- fastify/fast-uri: host confusion via percent-encoded authority delimiters
- WHATWG URL Standard
- RFC 3986: Uniform Resource Identifier (URI) Generic Syntax
