[HackerNotes Ep. 168] Client-Side Path Traversals Across Every Framework, with XSSDoctor

We dig CSPT across different frameworks with xssdoctor, discovering a nice bug in react router

Hacker TL;DR

  • React Router's useParams double URL decodes path parameters, and a case-sensitive regex on the React Source Code of matchPath means %252F (uppercase F) decodes to / while %252f (lowercase f) does not

  • Not all frameworks are equal: Vue and React are the most vulnerable to CSPT, Next.js and SvelteKit expose secondary context path traversal on the server side, while SolidStart is largely safe

  • Pre-production endpoints may serve uploaded HTML inline instead of as Content-Disposition: attachment, a reliable technique to bypass XSS mitigations on file upload

  • fetch() silently strips tab characters (%09), enabling WAF bypasses with payloads like %2F%2e%09%2e%5C

Who's XSS Doctor

XSSDoctor (JD) is a practicing cardiologist who found his way into bug bounty during the COVID-19 pandemic after stumbling on a "learn to hack in 12 hours" ad on Flipboard. It started with Hack The Box and CTFs, and it evolved into a deep specialization in client-side security, largely inspired by listening to the CTBB podcast. He has since become one of the community's most respected client-side researchers, running hackalongs, collaborating with top hunters, and building tools like DoctorScan for automated framework fingerprinting and CSPT source-to-sink analysis.

We do subs at $25, $10, and $5, premium subscribers get access to:

Hackalongs: live bug bounty hacking on real programs, VODs available
Live data streams, exploits, tools, scripts & un-redacted bug reports

XSS to PostMessage to Home Takeover

XSS Doctor was hacking a home automation platform whose AI assistant had full control over a user's house like alarms, locks, garage doors, everything. Here is the attack chain:

  1. PostMessage listener on the AI interface accepted messages from *.target.com

  2. XSS via file upload on a pre-production domain that served HTML inline instead of as an attachment

  3. The uploaded payload sends a postMessage to the AI, injecting an arbitrary prompt

  4. The AI executes the command: "turn off the alarm system"

The critical insight: pre-production endpoints often lack the Content-Disposition: attachment header that production enforces. JD found this pattern twice on the same target: first on one AI product, then on another where he also discovered a path parameter that let him upload to arbitrary directories, bypassing the download-only restriction.

Additionally, the AI's API had a CORS misconfiguration (Access-Control-Allow-Origin: *.target.com with credentials), meaning the XSS could directly hit the API without needing the postMessage gadget at all.

When testing file uploads for XSS, always check pre-production/staging endpoints.

Client-Side Path Enumeration: The Research

Xssdoctor produced a comprehensive research paper covering eight major frameworks: React, Next.js, Vue, Nuxt, Angular, Ember, SvelteKit, and SolidStart.

The Core Question

In single-page applications, the URL bar doesn't map to files on a server. A client-side router parses the path and routes to views. When a developer needs a dynamic value from the URL (like /settings/xssdoctor), they call a framework-specific function like useParams in React, useRoute in Vue, params in SvelteKit. The security question is: does that function URL-decode the value?

If it does, and the developer concatenates that decoded value into a fetch() call, the result is a Client-Side Path Traversal (CSPT).

The Framework Breakdown

Framework

Path Decoded?

Double URL Decode?

CSPT Risk

Secondary Path Traversal?

React Router

Yes (useParams)

Yes (uppercase F only!)

High

N/A

Vue Router

Yes

No

High

N/A

Angular

Partial

No

Medium

N/A

Next.js (server)

Yes (await params)

No

Low (client)

Yes

SvelteKit

Yes (server)

No

Low (client)

Yes

Ember

Yes

No

Medium

N/A

SolidStart

No

No

Low

No

Nuxt

Yes

No

High

N/A

The React Zero-Day Discovery

They discovered a nice 0day gadget on React during this episode. While demonstrating his React lab, JD showed that %252F (double-encoded slash) gets decoded back to /, but only when the F is uppercase.

The root cause is on the matchPath in React Router, there is a replace() call that maps %2F to /. This replacement is case-sensitive. It does not use the gi flag. The result:

  • %252F (uppercase F) → decoded to %2F → replaced to /path traversal works

  • %252f (lowercase f) → decoded to %2fnot replaced → no traversal

// React Router matchPath - the case-sensitive replace
// Only matches uppercase %2F, misses lowercase %2f
paramValue.replace(/%2F/g, "/")  // Missing the 'i' flag!

This behavior was actually reintroduced after a previous double-URL-encoding bug was reported and "fixed". The fix itself created this case-sensitivity gap.

So a good tips is to always test both uppercase and lowercase hex encoding in your CSPT payloads. If you've only been using %2f, you may have been missing vulnerabilities in React applications.

The Three Sources of CSPT

XSSDoctor's methodology identifies three injection sources in the URL:

  1. Path parameters (/settings/:userId): the most interesting and most commonly overlooked

  2. Query parameters (?id=value): almost always decoded by frameworks, a known vector

  3. Hash parameters (#fragment): less explored but framework-dependent

The key insight from the research: path parameters yield more impactful CSPTs than query parameters. Developers tend to put query values into query parameters and path values into path segments, meaning path injection flows directly into API path construction.

Testing Methodology

  1. Find endpoints with custom-looking paths: /home/xssdoctor

  2. Replace the dynamic segment with a unique string: /home/booyakasha

  3. Check Caido for API calls containing booyakasha (e.g., /api/booyakasha)

  4. If reflected, try /home/booyakasha%2f and check if the API request becomes /api/booyakasha/

  5. If path traversal works, assess impact: is the API state-changing (CSRF)? Does the response get inserted into the DOM (XSS)?

For XSS confirmation via CSPT, set a match-and-replace rule in your proxy that swaps a string in the API response with <img src=x>. If a broken image appears on the page, HTML injection is confirmed.

Secondary Context Path Traversal in Server-Side Frameworks

Frameworks with server-side rendering (Next.js, SvelteKit) introduce a different attack primitive. In Next.js:

  • useParams (client-side) does not auto-decode: safe from CSPT

  • await params (server-side) does auto-decode: vulnerable to secondary context path traversal

When a Next.js server component uses await params and passes the decoded value to an internal API, you get a server-side path traversal triggered from the URL bar. This is blind initially, but Next.js returns 500 errors on invalid paths, so it can be a useful oracle for detection.

On Next.js targets, test %2F..%2F payloads in path parameters. If you get a 500 for invalid traversal but a 200 for valid path reconstruction (value/../value), you've likely found secondary context path traversal.

The WAF Bypass and fetch() Quirks

JD shared his favorite WAF bypass payload: %2F%2e%09%2e%5C, leveraging the fact that fetch() silently strips tab characters (%09). This means .. with a tab inserted (%2e%09%2e) still resolves as a directory traversal after fetch processes it, but WAFs that pattern-match on .. or %2e%2e will miss it entirely.

The Fix That Won't Come

Unlike SQL injection, which was largely solved by prepared statements at the framework level, CSPT is fundamentally harder to fix:

  • At the source level: frameworks need to decode for legitimate functionality, breaking this breaks apps

  • At the sink level: fetch() receives a concatenated string and has no way to distinguish intended path segments from injected ones

Resources

That's it for the week, keep hacking!