fast-uri: path traversal via percent-encoded segments decoded before normalization
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.
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00145 pruva-verify GHSA-q3j6-qgpj-74h6 pruva-verify CVE-2026-6321 curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00145/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