What's the vulnerability?

fast-uri's normalize() and equal() decode percent-encoded path separators and dot segments before applying RFC 3986 dot-segment removal. Because of this ordering, an encoded %2e%2e%2f sequence is first decoded into a literal ../ and is then processed as a real dot segment during normalization.

The consequence is that two distinct input URIs collapse onto the same normalized path. Any path-prefix allowlist check built on top of normalize() / equal() can therefore be bypassed: an attacker supplies an encoded traversal sequence, the normalized result escapes the intended path prefix, and the allowlist comparison no longer reflects the real target.

Any code that forwards an attacker-influenced URI string into normalize() or equal() and then makes a path-prefix authorization decision on the result is exploitable.

Root Cause Analysis

# RCA Report: CVE-2026-6321 — fast-uri Path Traversal via Pre-Decoded Percent-Encoded Segments

## Summary

`fast-uri` versions 3.1.0 and earlier decode percent-encoded path separators (`%2F`) and dot segments (`%2E`) before applying RFC 3986 dot-segment removal during URI normalization. This allows an attacker to bypass path-prefix allowlist checks: an encoded traversal sequence such as `%2e%2e%2f` is first decoded into literal `../`, then processed as a real dot segment, causing the normalized path to escape the intended prefix. The `equal()` function suffers the same flaw, incorrectly reporting two distinct URIs as equal when one uses encoded traversal.

## Impact

- **Package**: `fast-uri` (npm)
- **Affected versions**: `<= 3.1.0`
- **Fixed version**: `3.1.1`
- **CVE**: CVE-2026-6321
- **CWE**: CWE-22 (Path Traversal)
- **Severity**: High (CVSS 7.5)
- **Consequences**: Any authorization logic that relies on `fast-uri.normalize()` or `fast-uri.equal()` to validate a path prefix can be bypassed. An attacker-supplied URI may normalize to a path outside the intended directory, leading to unauthorized access.

## Root Cause

The vulnerability stems from the order of operations in the `parse()` and `serialize()` pipeline:

1. **Vulnerable (v3.1.0)**: `parse()` calls `parsed.path = escape(unescape(parsed.path))`. The built-in `unescape()` converts `%2e` to `.` and `%2f` to `/`, so `%2e%2e%2f` becomes the literal string `../`. When `serialize()` later calls `removeDotSegments()` on this already-decoded path, the sequence is treated as a real parent-directory traversal instruction, collapsing `public/../admin` to `admin`.

2. **Fixed (v3.1.1)**: The code replaces the blanket `escape(unescape(...))` with `normalizePathEncoding()`, which deliberately preserves reserved path escapes such as `%2F` and `%2E`. Because these remain encoded, `removeDotSegments()` sees them as ordinary path data, not as dot segments, and the path stays confined.

The fix commit changes `index.js` and `lib/utils.js` and adds a regression test (`test/security-normalization.test.js`) that asserts `%2E%2E` stays encoded during normalization and that `equal()` no longer conflates encoded and literal separators.

## Reproduction Steps

The reproduction is fully automated by `repro/reproduction_steps.sh`.

What the script does:
1. Creates two isolated npm projects.
2. Installs `fast-uri@3.1.0` (vulnerable) in one project.
3. Runs a Node.js script that calls `normalize()` and `equal()` on URIs containing encoded traversal sequences (`%2e%2e`, `%2f`).
4. Installs `fast-uri@3.1.1` (fixed) in the second project.
5. Runs the identical test script.
6. Compares the outputs and writes a JSON verdict.

Expected evidence:
- **Vulnerable**: `normalize('http://example.com/public/%2e%2e/admin')` returns `http://example.com/admin`.
- **Fixed**: `normalize('http://example.com/public/%2e%2e/admin')` returns `http://example.com/public/%2E%2E/admin`.
- **Vulnerable**: `equal('http://example.com/public/%2e%2e/admin', 'http://example.com/admin')` returns `true`.
- **Fixed**: `equal(...)` returns `false`.

## Evidence

- **Vulnerable output log**: `logs/vuln-output.txt`
- **Fixed output log**: `logs/fixed-output.txt`
- **Validation verdict**: `logs/validation_verdict.json`

Key excerpts from the vulnerable run:
```
INPUT:  http://example.com/public/%2e%2e/admin
OUTPUT: http://example.com/admin

equal('http://example.com/public/%2e%2e/admin', 'http://example.com/admin') => true
```

Key excerpts from the fixed run:
```
INPUT:  http://example.com/public/%2e%2e/admin
OUTPUT: http://example.com/public/%2E%2E/admin

equal('http://example.com/public/%2e%2e/admin', 'http://example.com/admin') => false
```

Environment:
- Node.js runtime (any recent LTS)
- No external services, databases, or browsers required
- Pure in-process JavaScript test

## Recommendations / Next Steps

1. **Upgrade immediately** to `fast-uri@3.1.1` or later.
2. **Audit existing code** that uses `normalize()` or `equal()` for path-prefix or URI-allowlist decisions. Replace any ad-hoc workarounds with the patched library.
3. **Add regression tests** for encoded traversal sequences (`%2e%2e%2f`, `%2e%2e`, `%2f`) in your own URI-handling logic.
4. **Consider defense in depth**: wherever possible, validate resolved filesystem paths (e.g., using `path.resolve()` and checking the result is under a trusted root) rather than relying solely on URI normalization.

## Additional Notes

- **Idempotency**: The reproduction script was executed twice consecutively with identical results, confirming idempotency.
- **Edge cases tested**: single encoded slash (`%2f`), double encoded dot-dot (`%2e%2e`), chained sequences (`%2e%2e/%2e%2e`), and the `equal()` false-positive case. All behaved consistently with the vulnerability description.
One Command

Verify with pruva-verify

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

pruva-verify REPRO-2026-00145
or pruva-verify GHSA-q3j6-qgpj-74h6
or pruva-verify CVE-2026-6321
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-00145/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