How Unicode Hostname Canonicalization Bypass Happens in Node.js and How to Fix It
The Hidden Risk Inside Your URI Parser
URI parsing sounds like one of the most boring, solved problems in software engineering. Parse a string, hand back a structured object, done. But when internationalized domain names enter the picture, "solved" turns out to be a dangerous assumption — and CVE-2026-13676 in the widely-used fast-uri npm package is a sharp reminder of exactly why.
This post walks through the vulnerability, how it can be exploited, and the precise changes made to close it.
Summary
CVE-2026-13676 is a high-severity security policy bypass in fast-uri, a popular Node.js URI parsing library. Versions prior to 2.4.2, 3.1.3, and 4.0.1 fail to fully canonicalize Unicode hostnames, meaning a carefully crafted internationalized hostname can pass through fast-uri's parser looking like a different string than what security policies expect. The fix is a targeted version upgrade, enforced project-wide via a package.json overrides entry.
Introduction
The package-lock.json file in this repository locked fast-uri at version 3.1.2. That version contains a flaw in how it handles Unicode hostnames — specifically, it does not guarantee that an internationalized hostname is normalized to its canonical ASCII-compatible encoding (ACE / Punycode) form before returning the parsed host component. Any downstream code that uses that host value to make a security decision — an SSRF allow-list check, a redirect validator, an origin policy — is therefore operating on an un-normalized string and can be fooled.
Trivy's static analysis flagged this dependency as matching rule CVE-2026-13676, surfacing the issue before it could be exploited in production.
The Vulnerability Explained
What Is Unicode Hostname Canonicalization?
The Domain Name System only understands ASCII labels. Internationalized domain names (IDNs) — hostnames containing non-ASCII characters like münchen.de or 例え.jp — are encoded into ASCII via the Punycode algorithm before DNS resolution. The ASCII form of münchen.de is xn--mnchen-3ya.de.
A correctly implemented URI parser should normalize both representations to the same canonical form so that security comparisons are deterministic. If a parser returns münchen.de from one call and xn--mnchen-3ya.de from another, any string-equality check against an allow-list will produce inconsistent results.
The Specific Flaw in fast-uri 3.1.2
In fast-uri versions before the patch, the hostname component extracted from a URI was returned in whatever Unicode form it arrived in — it was not consistently converted to Punycode before being handed back to the caller. This means:
// Vulnerable behavior in fast-uri 3.1.2
const { host } = fastUri.parse('http://аррle.com/admin');
// host might be returned as the Unicode string 'аррle.com'
// rather than its Punycode equivalent 'xn--rrle-5cdd.com'
(Note: the Cyrillic characters а and р above are visually identical to the Latin a and p — a classic homograph attack.)
If your application then checks:
const ALLOWED_HOSTS = ['apple.com'];
if (!ALLOWED_HOSTS.includes(parsedUrl.host)) {
throw new Error('Host not allowed');
}
…the check passes because 'аррle.com' !== 'apple.com' at the byte level, even though both resolve to the same (or a visually indistinguishable) destination. The security policy is bypassed entirely.
Real-World Attack Scenario
Consider a Node.js service that:
1. Accepts a user-supplied URL for a webhook or outbound HTTP request.
2. Uses fast-uri to parse the URL and extract the hostname.
3. Checks the hostname against a block-list of internal IP ranges and sensitive internal services (e.g., metadata.internal, 169.254.169.254).
An attacker supplies a URL whose hostname uses Unicode characters that are visually identical to a blocked hostname but whose Unicode representation does not match the block-list string. fast-uri 3.1.2 returns the un-normalized Unicode form, the block-list check passes, and the service makes an outbound request to the attacker-controlled (or internal) destination — a classic Server-Side Request Forgery (SSRF) enabled by a canonicalization gap.
The Fix
What Changed in the Dependency
The fix is a version bump from fast-uri@3.1.2 to fast-uri@3.1.3. Here is the exact change in package-lock.json:
"node_modules/fast-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
- "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
+ "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
The new integrity hash (sha512-i70LwG…) cryptographically pins the resolved tarball, ensuring the patched code — and only the patched code — is installed.
Forcing the Upgrade for Transitive Dependencies
Because fast-uri is often pulled in as a transitive dependency (a dependency of a dependency), a direct version bump alone may not be sufficient. The fix also adds an overrides entry in package.json:
"overrides": {
"refractor": "4.8.0",
- "@opentelemetry/propagator-jaeger": "2.9.0"
+ "@opentelemetry/propagator-jaeger": "2.9.0",
+ "fast-uri": "3.1.3"
},
The overrides field in npm (v8.3+) instructs the package manager to replace every resolved instance of fast-uri in the dependency tree — regardless of which package requested it — with version 3.1.3. This is the correct pattern when you need to patch a transitive dependency that you do not own directly.
Why the fsevents Change?
The diff also marks fsevents as "dev": true:
"node_modules/fsevents": {
"version": "2.3.2",
+ "dev": true,
This is a housekeeping correction that ensures the macOS file-system events native module is not bundled into production artifacts. While unrelated to the CVE, it reduces the production attack surface by eliminating an unnecessary native dependency.
How the Patch Fixes the Root Cause
In fast-uri@3.1.3, hostname canonicalization is applied before the parsed components are returned. Unicode hostnames are converted to their Punycode equivalents, ensuring that any downstream comparison operates on a single, consistent representation. The patched behavior for the homograph example above would be:
// Patched behavior in fast-uri 3.1.3
const { host } = fastUri.parse('http://аррle.com/admin');
// host is now 'xn--rrle-5cdd.com' — the canonical Punycode form
// Block-list check against 'apple.com' correctly passes (different host)
// or fails (if the Punycode form is also blocked)
Security policy checks now receive a deterministic, canonical hostname regardless of how the input was encoded.
Prevention & Best Practices
1. Always Canonicalize Before Comparing
Never compare a hostname extracted from user input directly against an allow-list or block-list without first normalizing it. In Node.js, the built-in URL class performs this normalization:
const url = new URL('http://аррle.com/path');
console.log(url.hostname); // 'xn--rrle-5cdd.com' — already Punycode
For cases where you must use a third-party parser, add an explicit normalization step:
import { toASCII } from 'punycode'; // or use the 'punycode.js' npm package
const normalized = toASCII(parsedHost);
2. Pin Transitive Dependencies with overrides
When a security fix lands in a transitive dependency, use npm's overrides (or Yarn's resolutions) to force the patched version across the entire tree immediately, without waiting for every intermediate package to release an update:
"overrides": {
"fast-uri": ">=3.1.3"
}
3. Run Dependency Scanners in CI
Tools like Trivy, npm audit, and Snyk can detect known-vulnerable dependency versions before they reach production. Add them as a required CI step:
# Example: fail the build on high or critical findings
trivy fs --exit-code 1 --severity HIGH,CRITICAL .
4. Validate the Full URI, Not Just the Host
SSRF and redirect bypass vulnerabilities often exploit edge cases in URI parsing beyond the hostname — scheme confusion, IPv6 literals, embedded credentials. Use a defense-in-depth approach: validate scheme, normalize host, resolve path, and re-serialize before making any outbound request.
5. Relevant Standards
- OWASP SSRF Prevention Cheat Sheet — covers allow-listing, DNS rebinding, and canonicalization requirements.
- CWE-20: Improper Input Validation — the root class for failures to normalize user-supplied data before use.
- CWE-184: Incomplete List of Disallowed Inputs — applies when a block-list can be bypassed through encoding variations.
- RFC 5891 (IDNA 2008) — the authoritative specification for internationalized domain name canonicalization.
Key Takeaways
fast-uri@3.1.2(and earlier 2.x / 4.x versions) must not be used in any code path that feeds parsed hostnames into security policy checks — the un-normalized Unicode output is a bypass waiting to happen.- The
overridesfield inpackage.jsonis the correct tool for patching transitive dependencies you do not control directly; without it, other packages in the tree can silently re-introduce the vulnerable version. - Homograph attacks exploit the gap between visual appearance and byte-level representation — canonicalization to Punycode closes that gap before comparisons are made.
- A URI parsing library is a security boundary, not just a utility function. Its correctness directly determines whether allow-lists, block-lists, and SSRF defenses hold.
- Trivy's dependency scanning caught this before any code change was needed — integrating scanner output into automated PR workflows (as done here) compresses the time between vulnerability disclosure and remediation to near-zero.
How Orbis AppSec Detected This
- Source: User-supplied URI strings entering the application through HTTP request parameters or configuration values that accept webhook/callback URLs.
- Sink:
fast-uri'sparse()function returning an un-normalizedhostcomponent that is subsequently used in allow-list or block-list comparisons within the application's outbound request handling logic. - Missing control: No Punycode/ACE canonicalization was applied to the extracted hostname before it was compared against security policy strings, leaving the comparison vulnerable to Unicode homograph variants.
- CWE: CWE-20 (Improper Input Validation) / CWE-184 (Incomplete List of Disallowed Inputs)
- Fix: Upgraded
fast-urifrom3.1.2to3.1.3inpackage-lock.jsonand added"fast-uri": "3.1.3"to theoverridessection ofpackage.jsonto enforce the patched version across all transitive dependents.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-13676 is a textbook example of how a subtle implementation gap in a foundational library — one that handles something as routine as parsing a URL — can silently undermine every security control built on top of it. The fast-uri maintainers shipped a targeted fix in patch releases across all supported major versions, and the correct response is to adopt those patches immediately and enforce them throughout the dependency tree.
The broader lesson: treat URI parsing as a security-sensitive operation. Validate inputs, normalize hostnames to their canonical form before any comparison, and keep your dependency scanner in the critical path of every build. A one-line overrides entry and a patch-level version bump are a small price to pay for closing an SSRF or policy-bypass door that might otherwise go unnoticed until it is too late.
References
- CWE-20: Improper Input Validation
- CWE-184: Incomplete List of Disallowed Inputs
- OWASP Server-Side Request Forgery Prevention Cheat Sheet
- OWASP Input Validation Cheat Sheet
- npm Overrides Documentation
- RFC 5891 – Internationalized Domain Names in Applications (IDNA 2008)
- Semgrep rules for URI/SSRF patterns
- fix: upgrade fast-uri to 4.0.1, 3.1.3, 2.4.2 (CVE-2026-13676)