CVE-2026-86253: Verified Reproduction
CVE-2026-86253: h3 path traversal via unsanitized static file serving pathname
CVE-2026-86253 is verified against h3js/h3 · github. Vulnerability class: Path Traversal. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00363.
What Is CVE-2026-86253?
CVE-2026-86253 is a high-severity Path Traversal vulnerability affecting h3js/h3. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00363).
CVE-2026-86253 Severity
CVE-2026-86253 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
How to Reproduce CVE-2026-86253
pruva-verify REPRO-2026-00363 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00363/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-86253
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
HTTP pathname /%2e%2e/private-sentinel.txt sent with URL normalization disabled
- Node HTTP listener
- h3 event URL
- serveStatic
- getMeta/getContents filesystem callbacks
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for CVE-2026-86253
CVE-2026-86253 / GHSA-wr4h-v87w-p3r7 is an unauthenticated path-traversal vulnerability in h3's serveStatic() HTTP static-file handler. On the vulnerable Node.js path, an HTTP pathname containing percent-encoded dot segments remains unnormalized until serveStatic() calls decodeURI(). This changes %2e%2e into .. and passes the resulting traversal identifier to the configured getMeta() and getContents() callbacks. When those callbacks implement h3's documented filesystem-serving pattern with path.join(), the resolved path escapes the static root. In this run, h3 1.15.5 disclosed a unique file placed outside the static directory in two independent HTTP attempts, while h3 1.15.6 returned HTTP 404 to the same request in two fixed negative-control attempts.
- Package/component affected: npm package
h3, specificallyserveStatic()insrc/utils/static.tson Node.js deployments using the raw request-URL fast path. - Affected versions:
< 1.15.6on the 1.x line and>= 2.0.0, <= 2.0.1-rc.14on the 2.x release-candidate line. Patched versions are 1.15.6 and 2.0.1-rc.15. - Tested vulnerable target: Published
h3@1.15.5npm archive, SHA-256f99c479e7c12d0ad30c01e32fb76415a5eb8bf5f90ac0a036baf14d3f8432a7d. - Tested fixed target: Published
h3@1.15.6npm archive, SHA-2563b47bca4d3eacae8c61a6cc6a1049ec1c91c9a7cd29c710978977260a2dbe1ee. - Risk and consequences: A remote unauthenticated requester can read files accessible to the Node service account. Potential targets include application source,
.envfiles, API or database credentials, private keys, and operating-system files. The advisory rates the issue Moderate (CVSS 3.1 score 5.9), while the submitted ticket labels the operational risk High because confidentiality impact can be substantial.
Impact Parity
- Disclosed/claimed maximum impact: Remote arbitrary-file disclosure (
info_leak) through an h3serveStaticendpoint. - Reproduced impact from this run: A real localhost HTTP requester selected
/%2e%2e/private-sentinel.txt; vulnerable h3 decoded it to/../private-sentinel.txt, resolved that outsidepublic/, and returned the private sentinel in an HTTP 200 response. - Parity:
full. - Not demonstrated: Files that the service account cannot read remain inaccessible. The proof intentionally used a controlled sentinel rather than collecting real credentials or host-sensitive data. No write, code-execution, or privilege-escalation impact was claimed or tested.
Root Cause
The vulnerable statement in src/utils/static.ts was:
const originalId = decodeURI(withLeadingSlash(withoutTrailingSlash(event.url.pathname)));
On Node.js, h3 receives request pathnames through srvx's optimized URL path, which preserves percent-encoded dot segments rather than applying WHATWG URL dot-segment normalization. Consequently, the attacker input /%2e%2e/private-sentinel.txt reaches serveStatic() in encoded form. decodeURI() then converts %2e%2e into the literal segment .., yielding /../private-sentinel.txt. Vulnerable h3 performs no subsequent traversal normalization before calling the application-supplied static back end. The documented filesystem callbacks join this identifier with the configured public root, and Node's path resolution selects the sibling private file.
Fix commit 0e751b4059060f2ade01a0bdfd96b0f5ffc8a26d added resolveDotSegments() and applied it immediately after decoding. The script resolves the commit's vulnerable parent as 7791538e15ca22437307c06b78fa155bb73632a6, verifies that the parent lacks this call, verifies that the fixed commit contains it, and records the exact diff at bundle/repro/evidence/source_fix.diff. The fix collapses traversal segments without allowing resolution above the static-root identifier.
A distinct later advisory, GHSA-72gr-qfp7-vwhw, concerns a double-decoding bypass through %252e%252e and was fixed in 1.15.9. That follow-up is intentionally left for the required variant-analysis stage and does not change the base CVE result against the originally patched 1.15.6 control.
Reproduction Steps
- Run
bash bundle/repro/reproduction_steps.shfrom any directory. The script derives its bundle path from its own location orPRUVA_ROOT. - The script reads
bundle/project_cache_context.json, uses/pruva/project-cache/repowhen the prepared cache is available, and otherwise falls back tobundle/artifacts/h3. - It clones or reuses
https://github.com/h3js/h3, fetches fix commit0e751b4059060f2ade01a0bdfd96b0f5ffc8a26d, resolves its parent, and checks the fixing hunk on both sides. - It retrieves and installs the exact published h3 1.15.5 and 1.15.6 archives. The generated Node server uses the real h3
createApp,eventHandler,serveStatic, andtoNodeListenercode with filesystem callbacks. - For each attempt, it starts a fresh bounded Node HTTP listener, verifies a legitimate
health.txtrequest, then sends the raw pathname/%2e%2e/private-sentinel.txtusingcurl --path-as-is. - It requires two vulnerable attempts to return HTTP 200 with the out-of-root sentinel and two fixed attempts to return a non-200 response without that sentinel. It exits nonzero on any mismatch.
- Before success, it writes
bundle/repro/runtime_manifest.jsonwith target/runtime identity and SHA-256 mappings for every finalized proof artifact.
Expected terminal evidence includes:
[+] CONFIRMED vulnerable attempt 1: HTTP 200 disclosed CVE-2026-86253-OUTSIDE-STATIC-ROOT-7e82b84d
[+] CONFIRMED vulnerable attempt 2: HTTP 200 disclosed CVE-2026-86253-OUTSIDE-STATIC-ROOT-7e82b84d
[+] NEGATIVE CONTROL fixed attempt 1: HTTP 404, no sentinel disclosure
[+] NEGATIVE CONTROL fixed attempt 2: HTTP 404, no sentinel disclosure
Evidence
Runtime manifest:
bundle/repro/runtime_manifest.jsonis strict JSON and binds 22 immutable artifacts by SHA-256. It recordsentrypoint_kind=endpoint,service_started=true,healthcheck_passed=true, andtarget_path_reached=true.Attacker request:
bundle/repro/evidence/vulnerable_attempt_1_request.txtrecordsGET /%2e%2e/private-sentinel.txt HTTP/1.1across the actual HTTP socket.Vulnerable server path:
bundle/repro/evidence/vulnerable_attempt_1_server.logrecords:GET_META id="/../private-sentinel.txt" resolved="/pruva/project-cache/package/h3-cve-2026-86253/runtime/private-sentinel.txt" GET_CONTENTS id="/../private-sentinel.txt" resolved="/pruva/project-cache/package/h3-cve-2026-86253/runtime/private-sentinel.txt"The configured static root is the sibling
runtime/publicdirectory, so this proves the selected file escaped that root.Vulnerable response:
bundle/repro/evidence/vulnerable_attempt_1_response_headers.txtrecordsHTTP/1.1 200 OK;bundle/repro/evidence/vulnerable_attempt_1_response_body.txtcontains the exact outside-root sentinelCVE-2026-86253-OUTSIDE-STATIC-ROOT-7e82b84d.Repeated vulnerable proof: The independently started second endpoint has equivalent evidence in
bundle/repro/evidence/vulnerable_attempt_2_*.Fixed negative control:
bundle/repro/evidence/fixed_attempt_1_response_headers.txtrecordsHTTP/1.1 404 Not Found, its body does not contain the sentinel, andbundle/repro/evidence/fixed_attempt_1_server.logcontains no traversal identifier or private-file callback. Attempt 2 independently repeats this result inbundle/repro/evidence/fixed_attempt_2_*.Source and target identity:
bundle/repro/evidence/source_fix.diffrecords the exact vendor fix.bundle/repro/evidence/target_identity.txtrecords package digests, source commits, Node v24.18.0, Node executable SHA-25641a74efb34cbde5c7632cdac0cf8bd1a14d0b8d73dc1e82755014d9a9ce70f5c, Linux, and x86_64.Diagnostic log:
bundle/logs/reproduction_steps.logrecords the complete most recent script execution but is deliberately not hashed while an activeteeis writing it.
Recommendations / Next Steps
- Upgrade the 1.x package line to at least h3 1.15.9, not merely 1.15.6, so both the original CVE and the later double-decoding bypass are addressed. On the 2.x line, use a release that includes both hardening changes rather than a pre-fix release candidate.
- Canonicalize the decoded URL path exactly once before any static-backend callback and reject or collapse
.and..segments before filesystem or URL resolution. - Treat callback implementations as untrusted with respect to containment: resolve the candidate path to an absolute path and prove it lies beneath the configured root with separator-aware logic before opening it.
- Add regression coverage for lowercase/uppercase percent-encoded dots, single- and double-encoded traversal, encoded separators, mixed separators, repeated slashes, root underflow, query strings, prefixes resembling the root, and non-filesystem static back ends.
- Retain a fixed-version HTTP negative control in integration tests; unit testing only
resolveDotSegments()would not cover the raw Node request-URL boundary and decoding order.
Additional Notes
- Idempotency: The final script passed twice consecutively in this run. Each script invocation itself starts two clean vulnerable and two clean fixed listeners; thus the final evidence represents four independent service processes per invocation.
- Real boundary: This is not a direct call to
serveStatic(). The attacker-controlled bytes cross a real Node TCP/HTTP listener and flow through h3's request-event and static-serving path before filesystem access. - Sanitizers: None were used or needed; information disclosure was directly visible in the HTTP response.
- Limitations: The proof targets Linux/Node and the vulnerable 1.x line. The advisory notes that runtimes supplying an already normalized WHATWG
URLmay not follow the vulnerable Node fast path. The exact follow-up%252e%252ebypass belongs to variant analysis.
CVE-2026-86253 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-86253
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-86253
FAQ: CVE-2026-86253
Is CVE-2026-86253 exploitable?
How severe is CVE-2026-86253?
What type of vulnerability is CVE-2026-86253?
How can I reproduce CVE-2026-86253?
Is the CVE-2026-86253 reproduction verified?
References for CVE-2026-86253
Authoritative sources for CVE-2026-86253 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.