Press enter or click to view image in full size
Hello! Lucas Cebrero here, Security Consultant at Kulkan. As part of an internal training activity I’ve been working on a small lab to learn about Client Side Path Traversal attacks. I wanted to share it with you along with a short blogpost. I hope that you enjoy it.
Introduction
Pentesters used to exploit CSRF almost everywhere with relative ease. Even when anti-CSRF tokens were introduced, secure implementations were hard to come by. Then came the SameSite cookie flag, which significantly raised the bar, and when browsers started setting SameSite to Lax by default, things got even harder.
The final blow seemed to come with applications that authenticate exclusively via header-based authentication methods such as JWTs in HTTP headers, as is commonly seen nowadays. Since these tokens aren’t automatically sent by the browser like cookies, the classic CSRF attack model no longer applies.
But a few years ago, a clever technique emerged that allowed pentesters to resurrect CSRF attacks in these auth header scenarios, Client-Side Path Traversal. I’d heard of this technique before, but decided to dive deeper, build the simplest possible lab, and understand exactly how it works.
Let’s take a closer look.
CSRF Protections
CSRF
First of all, why was CSRF such a widespread issue? Because browsers automatically include cookies for a site whenever a request matches the Origin. If you’re unfamiliar with the concept of Origin, I recommend that you read Mozilla’s documentation over at: https://developer.mozilla.org/en-US/docs/Glossary/Origin
Press enter or click to view image in full size
This expected behavior could be abused by attackers. If a user visited a malicious page while logged into another site, the attacker could trigger an authenticated request to that site, often performing state-changing actions, like updating an email address.
Press enter or click to view image in full size
Anti-CSRF tokens
To defend against this, developers began adding anti-CSRF tokens. These are random values which are generated server-side and included in the requests made by users that the server has to verify before performing any action. For these tokens to be effective, they must be:
- Unique per user session.
- Secret
- Unpredictable (large random value generated by a secure method)
In practice, getting all these right is harder than it sounds, and insecure implementations have been common.
SameSite cookie flag
A newer browser defense is the SameSite cookie attribute, which controls whether cookies are sent with cross-site requests. It has three possible settings:
- None: cookies are sent with all requests, just like before.
- Lax: cookies are sent for same-site requests and safe cross-site requests like GET. They are not sent for POST, PUT, or DELETE from other Origins.
- Strict: cookies are only sent if the request originates from the exact same Origin (same protocol, host, and port).
Originally, this flag had to be set explicitly by developers. Now, modern browsers default to Lax if the attribute is missing, reducing CSRF risk for cookie-based authentication.
Authentication via HTTP headers (JWT)
Beyond SameSite cookies, many modern applications, especially Single-Page Applications (SPAs), have moved to using JSON Web Tokens (JWTs) for authentication. These tokens are typically stored in localStorage or sessionStorage and sent to the server via an Authorization header.
This approach eliminates the classic CSRF risk associated with cookies because:
- Unlike cookies, JWTs placed in the Authorization header are not sent automatically with every request to their Origin.
- JWTs in headers are only sent when the application’s JavaScript code explicitly adds them, making it impossible for a cross-site attacker to trigger an authenticated request without running code in the victim’s browser.
In other words, for a while, it looked like CSRF was finally dead in these “header-auth” scenarios. However, in 2022 an overlooked issue with seemingly low severity made it into PortSwigger’s Top 10 Web Hacking Techniques: Client-Side Path Traversal. Shortly after, Doyensec published their excellent research “Exploiting Client-Side Path Traversal: CSRF is Dead, Long Live CSRF”, which inspired me to take a closer look at this vulnerability.
What is Client-Side Path Traversal (CSPT)?
Modern Single-Page Applications (SPAs) rely heavily on the front-end. Routing, state management, and even sensitive operations like updating user profiles or changing passwords are often initiated entirely by JavaScript in the browser.
In these applications, the frontend code dynamically constructs API requests to send to the backend. For example, building a URL like /api/user/123/orders based on the route or query parameters in the current page like /#/orders/?userId=123.
This is convenient for development, but when user-controlled values are used in this path-building process without proper validation, an attacker can tamper with them to reach unintended endpoints.
Client-Side Path Traversal occurs when a frontend application takes a user-controlled path (or part of it), normalizes it, and sends it as part of an internal API request.
By manipulating this path, e.g., using ../ sequences, an attacker can make the frontend navigate to or call endpoints they shouldn’t suppose to.
Example:
https://test-site.local/#/profile?id=../../../../api/update-password
If the application’s JavaScript takes id and concatenates it into a request URL, it might end up calling /api/update-password without the user ever directly navigating there.
In this example, we’ll call “source” to the query parameter id and “sink” to the password update API /api/update-password.
The source is the attacker-controlled input that feeds into the vulnerable path construction.
And the sink, the sensitive or exploitable endpoint reached because of the traversal.
Why CSPT defeats JWT-in-header protections
Normally, an attacker can’t make the victim’s browser send a JWT in a request — unless they can run JavaScript in the victim’s Origin (e.g., via XSS).
But with CSPT, the attacker doesn’t need to inject code, they only need to trick the victim into visiting a crafted URL.
The frontend code itself will:
- Resolve the manipulated path.
- Send the request to the sink.
- Automatically include the JWT in the Authorization header (or the header to use in common agreement between the client and the server).
From the backend’s perspective, it looks like a legitimate request from the authenticated user, a classic CSRF scenario brought back to life.
Walkthrough: Exploiting CSPT in the Lab
To really understand CSPT, I built a minimal vulnerable lab. The goal was to keep it simple enough to grasp the mechanics, but realistic enough to reflect issues you might find in modern apps.
The lab is available at:
Lab setup
- Frontend: React.js SPA
- Backend: Node.js REST API
Features:
- User creation, deletion, and modification with a validation feature.
- User profile viewing
- Admin panel to promote members to administrators
In real-world testing, very attractive CSRF targets would be the features for user deletion and user promotion. Because the app uses JWT authentication via HTTP headers, a classic CSRF which relies on cookie-based authentication is impractical. We wouldn’t be able to make the browser send the JWT for us unless… the frontend itself did it for us.
Finding a source
While browsing the app, a password confirmation feature can be found upon updating the email address. In a real system, this would send a confirmation link to the user’s email. However, in our lab, the link is simply displayed via a JavaScript alert().
Press enter or click to view image in full size
When visiting this link, we can see that the frontend makes a request like:
fetch(`${config.apiBaseURL}/api/users/${userId}/profile`, {method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verified: true })
})
Here, userId is taken directly from the URL parameter with no sanitization nor validation and concatenated into the API path.
That’s our source.
From source to sink
If we replace userId with a traversal sequence ..%2f..%2f.. the resulting request path changes from /api/users/<id>/profile to /profile. And because the frontend still attaches the Authorization header, we can make it call any API endpoint the authenticated user has access to.
Press enter or click to view image in full size
Crafting our exploit
The JavaScript code appends /profile to the request path. To remove it, we can append a # (fragment) or ? (query string) to our payload. The browser will treat anything after that as a fragment identifier or query string parameter respectively, excluding it from the path sent to the server.
Press enter or click to view image in full size
There are two interesting endpoints in the backend that are perfect candidates to use as a sink.
The /admin/promote/<USER-ID> endpoint that promotes a user to admin, and the /users/<USER-ID>/delete which deletes a user.
Since the backend doesn’t care about extra parameters in the request body and only the path matters, we can target them directly.
The final payloads will look like the following:
Payload to promote
http://localhost.lan:3001/validate?userId=..%2fadmin%2fpromote%2fATTACKER-USERID%3fPayload to delete
http://localhost.lan:3001/validate?userId=VICTIM-USERID%2fdelete%3fWhen the victim clicks one of these links while logged in, the application’s own JavaScript:
- Resolves the manipulated path.
- Sends the request with the victim’s JWT in the Authorization header.
- Executes the action without the victim’s consent.
In this example, an admin user clicked on the following URL, resulting in promoting an attacker with userId 092dc69583ca40b397cd7cd4485ddc4d
http://localhost.lan:3001/validate?userId=..%2fadmin%2fpromote%2f092dc69583ca40b397cd7cd4485ddc4d%3fPress enter or click to view image in full size
At this point, we’ve bypassed the JWT in header “protection” that comes as a consequence of using header-based authentication and pulled off a CSRF-style attack entirely via the frontend’s path handling.
Conclusion
As we can see, CSRF can still be exploitable through Client-Side Path Traversal, even with SameSite cookies and in applications that use header-based authentication. There’s no single silver bullet that fixes every case, so the safest approach is to avoid designs where client-side variables determine the request path sent to the server.
For applications that already rely on this pattern, it’s important to enforce strict validation on the backend by clearly defining which parameters and paths are expected, and by rejecting requests that include unexpected values. Doing so significantly reduces the likelihood of attackers finding a meaningful CSPT sink to exploit.
Additionally, client-side input validation is also useful because it adds another layer of complexity for an attacker. Even though client-side validation can be bypassed by modifying the DOM or altering the code, it’s unrealistic for an attacker to rely on a victim doing that themselves, which makes exploiting a CSPT through this vector significantly harder. Still, client-side validation should always be seen as a complement to server-side protections, not a replacement.
Lucas Cebrero Lell [LinkedIn] [X]
Security Consultant @ Kulkan
Resources:
- MDN Web Docs: SameSite cookies: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
- OWASP CSRF Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#rule---use-cryptographically-secure-pseudo-random-number-generators-csprng
- Doyensec CSPT to CSRF: https://blog.doyensec.com/2024/07/02/cspt2csrf.html
- Curated list of CSPT resources: https://blog.doyensec.com/2025/03/27/cspt-resources.html
About Kulkan
Kulkan Security (www.kulkan.com) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at www.kulkan.com
More on Kulkan at:
Subscribe to our newsletter at: