# REPRO-2026-00145: fast-uri: path traversal via percent-encoded segments decoded before normalization ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00145 GHSA: GHSA-q3j6-qgpj-74h6 CVE: CVE-2026-6321 ## Package Name: fast-uri Ecosystem: npm Affected: <= 3.1.0 Fixed: 3.1.1 ## Root Cause # 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. ## Reproduction Details Reproduced: 2026-05-22T18:01:34.613Z Duration: 608 seconds Tool calls: 154 Turns: 132 Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00145 pruva-verify GHSA-q3j6-qgpj-74h6 pruva-verify CVE-2026-6321 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00145&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00145/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-q3j6-qgpj-74h6 - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-6321 - Source: https://github.com/fastify/fast-uri.git ## Artifacts - repro/rca_report.md (analysis, 4818 bytes) - repro/reproduction_steps.sh (reproduction_script, 6250 bytes) - vuln_variant/rca_report.md (analysis, 6053 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 5963 bytes) - bundle/context.json (other, 2892 bytes) - bundle/metadata.json (other, 667 bytes) - bundle/ticket.md (ticket, 3389 bytes) - repro/validation_verdict.json (other, 580 bytes) - repro/fixed/variant_tests.js (other, 2356 bytes) - repro/fixed/package.json (other, 76 bytes) - repro/fixed/test-vuln.js (other, 776 bytes) - repro/vuln/variant_tests.js (other, 2356 bytes) - repro/vuln/package.json (other, 75 bytes) - repro/vuln/test-vuln.js (other, 776 bytes) - vuln_variant/root_cause_equivalence.json (other, 1352 bytes) - vuln_variant/patch_analysis.md (documentation, 5052 bytes) - vuln_variant/variant_manifest.json (other, 2532 bytes) - vuln_variant/validation_verdict.json (other, 1857 bytes) - vuln_variant/source_identity.json (other, 769 bytes) - vuln_variant/test_env/latest312/variant_test.js (other, 3081 bytes) - vuln_variant/test_env/latest312/package.json (other, 273 bytes) - vuln_variant/test_env/fixed311/variant_test.js (other, 3081 bytes) - vuln_variant/test_env/fixed311/package.json (other, 272 bytes) - vuln_variant/test_env/vuln310/variant_test.js (other, 3081 bytes) - vuln_variant/test_env/vuln310/package.json (other, 271 bytes) - logs/fixed-output.txt (other, 561 bytes) - logs/fixed_311_test.log (log, 1202 bytes) - logs/vuln_310_test.log (log, 1186 bytes) - logs/variant_test.js (other, 3081 bytes) - logs/variant_tests.js (other, 2356 bytes) - logs/vuln-output.txt (other, 512 bytes) - logs/fixed_variant_tests.txt (other, 2114 bytes) - logs/latest_312_test.log (log, 1181 bytes) - logs/validation_verdict.json (other, 580 bytes) - logs/test-vuln.mjs (other, 771 bytes) - logs/vuln_variant_tests.txt (other, 1926 bytes) - logs/test-vuln.js (other, 776 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00145 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00145/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00145 ## 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