fast-uri: host confusion via percent-encoded authority delimiter in normalize()
What's the vulnerability?
fast-uri's normalize() decodes percent-encoded authority delimiters that
appear inside the host and then re-emits them raw. The @ character is a
userinfo separator in RFC 3986 authority syntax, so re-emitting a decoded @
changes how the authority is parsed.
An attacker supplies a host string combining an allowed domain, an encoded @
(%40), and a second domain — e.g. allowed.com%40attacker.com. The
vulnerable build decodes the %40 and emits a raw @, so the normalized
authority becomes allowed.com@attacker.com: allowed.com is reinterpreted as
userinfo and the effective host is the second domain, attacker.com.
Any host-allowlist, SSRF guard, or redirect-validation check built on top of
normalize() is therefore defeated — the validation logic sees the allowed
domain while the effective target is attacker-controlled.
Root Cause Analysis
# Root Cause Analysis: CVE-2026-6322
## Summary
`fast-uri`'s `normalize()` function decodes percent-encoded authority delimiter characters (specifically `@`) that appear inside the host component and then re-emits them as raw characters. When an attacker supplies a URI such as `https://allowed.com%40attacker.com/`, the vulnerable build decodes `%40` to `@`, producing `https://allowed.com@attacker.com/`. Under RFC 3986 authority syntax, the text before `@` is reinterpreted as userinfo and the effective host becomes `attacker.com`. This host confusion defeats any host-allowlist, SSRF guard, or redirect-validation logic built on top of `normalize()`.
## Impact
- **Package**: `fast-uri` (npm)
- **Affected versions**: `<= 3.1.1`
- **Fixed version**: `3.1.2`
- **Risk level**: High (CVSS 7.5)
- **Consequences**: SSRF bypass, open-redirect bypass, and general defeat of URI-based access controls that rely on normalized host values.
## Root Cause
In `fast-uri` `<= 3.1.1`, the `parse()` function unconditionally calls `unescape(parsed.host)` during normalization. `unescape()` decodes all percent-encoded sequences, including RFC 3986 "gen-delims" such as `@`, `:`, `/`, `?`, and `#`. These delimiters are structurally significant in the authority component. After decoding, `serialize()` / `recomposeAuthority()` re-emits them as raw characters, changing the parsed structure of the URI.
The fix in `3.1.2` introduces `reescapeHostDelimiters()` in `lib/utils.js`. After `unescape()` runs on the host, this new function scans for decoded gen-delims and re-escapes them (`@` → `%40`, `:` → `%3A`, etc.). Additionally, `normalizeStringWithStatus()` detects malformed authorities or out-of-range ports and, when found, returns the original input unchanged rather than canonicalizing it into a different valid URI. The key fix commit is the `v3.1.1..v3.1.2` tag range in the `fastify/fast-uri` repository.
## Reproduction Steps
1. Run `repro/reproduction_steps.sh`.
2. The script installs `fast-uri@3.1.1` (vulnerable) and `fast-uri@3.1.2` (fixed) into isolated scratch directories.
3. It invokes `normalize('https://allowed.com%40attacker.com/')` against both versions and parses the resulting URI.
4. Expected evidence:
- **Vulnerable (3.1.1)**: normalized URI is `https://allowed.com@attacker.com/`, parsed `host` = `attacker.com`, parsed `userinfo` = `allowed.com`.
- **Fixed (3.1.2)**: normalized URI is `https://allowed.com%40attacker.com/`, parsed `host` = `allowed.com%40attacker.com`, parsed `userinfo` = `undefined`.
## Evidence
- Log files:
- `logs/vuln_result.json` — captured result from the vulnerable build.
- `logs/fixed_result.json` — captured result from the fixed build.
- Key excerpts from reproduction run:
```
VULNERABLE BUILD (3.1.1)
Normalized URI: https://allowed.com@attacker.com/
Parsed host: attacker.com
Parsed userinfo: allowed.com
FIXED BUILD (3.1.2)
Normalized URI: https://allowed.com%40attacker.com/
Parsed host: allowed.com%40attacker.com
Parsed userinfo: undefined
```
- Environment: Node.js (npm-based) running on Linux with `fast-uri` installed from the public npm registry.
## Recommendations / Next Steps
- **Upgrade immediately** to `fast-uri@3.1.2` or later.
- **If upgrading is not possible**: wrap `normalize()` output with an additional parse-and-validate step that rejects any host containing `@` or other authority delimiters after normalization.
- **Testing recommendations**: add regression tests for percent-encoded delimiters inside the host (`%40`, `%3A`, `%2F`, `%3F`, `%23`) to ensure they remain escaped through `normalize()`, `parse()`, `serialize()`, and `equal()`.
## Additional Notes
- The reproduction script is idempotent: it passed two consecutive runs without modification.
- Edge cases tested implicitly by the fix include double-encoded sequences (`%2540` → `%40`, not `@`) and IPv6 hosts where colon re-escaping is skipped.
- The `equal()` function was also hardened in `3.1.2` to return `false` when either input is malformed, preventing accidental equivalence between `trusted.com%40evil.com` and `trusted.com@evil.com`.
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00146 pruva-verify GHSA-v39h-62p7-jpjc pruva-verify CVE-2026-6322 curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00146/artifacts/reproduction_steps.sh Make executable
chmod +x reproduction_steps.sh Run the script
./reproduction_steps.sh How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available