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`.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00146
or pruva-verify GHSA-v39h-62p7-jpjc
or pruva-verify CVE-2026-6322
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00146/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available