- Critical Thinking - Bug Bounty Podcast
- Posts
- [HackerNotes Ep. 168] Client-Side Path Traversals Across Every Framework, with XSSDoctor
[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
useParamsdouble URL decodes path parameters, and a case-sensitive regex on the React Source Code ofmatchPathmeans%252F(uppercase F) decodes to/while%252f(lowercase f) does notNot 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 uploadfetch()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:
PostMessage listener on the AI interface accepted messages from
*.target.comXSS via file upload on a pre-production domain that served HTML inline instead of as an attachment
The uploaded payload sends a
postMessageto the AI, injecting an arbitrary promptThe 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 ( | Yes (uppercase F only!) | High | N/A |
Vue Router | Yes | No | High | N/A |
Angular | Partial | No | Medium | N/A |
Next.js (server) | Yes ( | 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%2f→ not 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:
Path parameters (
/settings/:userId): the most interesting and most commonly overlookedQuery parameters (
?id=value): almost always decoded by frameworks, a known vectorHash 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
Find endpoints with custom-looking paths:
/home/xssdoctorReplace the dynamic segment with a unique string:
/home/booyakashaCheck Caido for API calls containing
booyakasha(e.g.,/api/booyakasha)If reflected, try
/home/booyakasha%2fand check if the API request becomes/api/booyakasha/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 CSPTawait 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!
