# REPRO-2026-00146: fast-uri: host confusion via percent-encoded authority delimiter in normalize() ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00146 GHSA: GHSA-v39h-62p7-jpjc CVE: CVE-2026-6322 ## Package Name: fast-uri Ecosystem: npm Affected: <= 3.1.1 Fixed: 3.1.2 ## Root Cause # 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`. ## Reproduction Details Reproduced: 2026-05-22T18:01:54.838Z Duration: 618 seconds Tool calls: 127 Turns: 97 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00146 pruva-verify GHSA-v39h-62p7-jpjc pruva-verify CVE-2026-6322 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00146&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00146/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-v39h-62p7-jpjc - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-6322 ## Artifacts - repro/rca_report.md (analysis, 4167 bytes) - repro/reproduction_steps.sh (reproduction_script, 3388 bytes) - vuln_variant/rca_report.md (analysis, 7406 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 4529 bytes) - bundle/context.json (other, 2921 bytes) - bundle/metadata.json (other, 685 bytes) - bundle/ticket.md (ticket, 3159 bytes) - repro/validation_verdict.json (other, 1363 bytes) - vuln_variant/patch_analysis.md (documentation, 4984 bytes) - vuln_variant/variant_manifest.json (other, 2779 bytes) - vuln_variant/validation_verdict.json (other, 2128 bytes) - vuln_variant/scratch/package.json (other, 53 bytes) - logs/vuln_result.json (other, 182 bytes) - logs/fixed_result.json (other, 189 bytes) - logs/vuln_variant/fixed_variant_results.json (other, 2124 bytes) - logs/vuln_variant/vuln_variant_results.json (other, 2012 bytes) - logs/vuln_variant/run_variants.js (other, 1931 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00146 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00146/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00146 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev