- Một lỗ hổng CSPT kết hợp prototype chain bypass giúp kẻ tấn công chiếm hoàn toàn tài khoản mà không cần OTP hợp lệ.
- Bug bounty payout đạt $15,000 trên HackerOne.
- Server nhận email mới qua query parameter thay vì request body - một sai lầm nhỏ nhưng đủ để leo thang thành account takeover.
- OTP check dùng plain object lookup không có Object.hasOwn() khiến __proto__ trả về truthy mà không cần mã hợp lệ.
TL;DR
Một lỗ hổng CSPT (Client-Side Path Traversal) kết hợp với kỹ thuật bypass 2FA qua prototype chain JavaScript đã cho phép kẻ tấn công chiếm hoàn toàn tài khoản nạn nhân - mà không cần OTP hợp lệ. Toàn bộ chuỗi tấn công được thực hiện bởi nhà nghiên cứu bảo mật và báo cáo qua HackerOne với bug bounty $15,000. Hai lỗ hổng độc lập khi đứng riêng lẻ, nhưng khi kết hợp trở thành một trong những chuỗi tấn công account takeover hoàn chỉnh nhất từng được ghi nhận.

Bốn bước từ invite link đến full access
Toàn bộ chuỗi tấn công diễn ra như sau:
- CSPT - đổi email nạn nhân: Kẻ tấn công gửi invite link độc. Khi nạn nhân click, browser tự động gửi
PUT /api/v2/user?email=attacker@evil.com- đổi email tài khoản sang địa chỉ do kẻ tấn công kiểm soát, trong phiên đã xác thực của chính nạn nhân. - Password reset: Kẻ tấn công trigger quy trình đặt lại mật khẩu. Link reset gửi về inbox của họ, đặt mật khẩu mới.
- 2FA bypass: Server yêu cầu OTP qua SMS. Kẻ tấn công gửi header
X-2FA-Code: __proto__thay vì mã hợp lệ. Server trả200 OKvà cấp session token. - Full account takeover: Tài khoản bị chiếm hoàn toàn.
CSPT - Browser trở thành vũ khí
Client-Side Path Traversal xảy ra khi front-end đọc tham số từ URL mà không kiểm tra hay sanitize, rồi ghép thẳng vào đường dẫn API request. Hàm handleTeamInvite trong ứng dụng đọc teamId và inviteId trực tiếp từ window.location.search mà không có bất kỳ allowlist hay validation nào.
Payload kẻ tấn công chèn vào invite URL:
teamId = ../../../api/v2/user%3femail=attacker%40evil.com%26a=ainviteId = ../
Khi front-end ghép tham số vào path, browser normalize ../../../ và request thực tế đến PUT /api/v2/user?email=attacker@evil.com. Phần %3f (= ?) và %26 (= &) được URL-encode để qua lần parse đầu, rồi browser decode chúng thành URL separators thực sự khi gửi request. Tham số &a=a đóng vai trò filler để hấp thụ phần /invites/../ thừa vào một query param vô hại.
Điểm then chốt phía backend: server không yêu cầu email mới trong request body theo chuẩn HTTP PUT, mà chấp nhận nó từ query parameter. Một quyết định thiết kế nhỏ nhưng đủ để biến CSPT thành account takeover.
Tại sao CSPT cực kỳ nguy hiểm: request được phát ra từ chính browser của nạn nhân với credentials của họ, nên SameSite cookies và CSRF tokens hoàn toàn vô hiệu. Kẻ tấn công không cần đánh cắp cookie hay bypass CORS.
Bypass 2FA không cần OTP - đọc mà không ghi
Sau khi reset password và login, server yêu cầu OTP qua SMS. Backend kiểm tra OTP với logic tương tự:
if (pendingCodes[code]) {
// phát session
}
Kẻ tấn công gửi X-2FA-Code: __proto__. Đây không phải prototype pollution (không ghi gì lên prototype) - mà là một khai thác read-side thuần túy dựa trên cơ chế property lookup của JavaScript.
Cơ chế diễn ra từng bước:
pendingCodes["__proto__"]không có own key nào tên là__proto__- JavaScript leo lên prototype chain theo thuật toán
[[Get]]của ECMAScript __proto__là accessor property trênObject.prototype, getter trả về chínhObject.prototypeObject.prototypelà một object - luôn truthyif (Object.prototype)- điều kiện pass, server cấp session token
Nhiều cách check tưởng an toàn nhưng vẫn bị bypass vì đều walk prototype chain:
key in obj- dùng[[HasProperty]], vẫn leo chainReflect.has(obj, key)- tương đươnginobj[key] !== undefined- vẫn invoke[[Get]]
Fix đúng cách:
Object.hasOwn(pendingCodes, code)- chỉ check own properties (ES2022)- Dùng
Mapthay plain object -Mapdùng[[MapData]]slot, không bao giờ walk prototype chain Object.create(null)- tạo object không có prototype
Không phải lần đầu - CSPT trong thực tế
CSPT đã xuất hiện trong nhiều sản phẩm lớn, một số trường hợp leo thang đến RCE:
| Sản phẩm | CVE | Tác động tệ nhất |
|---|---|---|
| Mattermost | CVE-2023-45316 | RCE qua install malicious plugin |
| Mattermost | CVE-2023-6458 | 1-click POST CSRF không cần tương tác |
| Rocket.Chat | - | 1-click logout CSRF |
| GitLab | - | 1-click CSRF |
| - | Trường hợp đầu tiên ghi nhận (pre-2021) |
Prototype chain truthy bypass ít được biết đến hơn nhưng nguy hiểm ngang ngửa prototype pollution: CVE-2019-7609 (Kibana) cho phép RCE qua việc ghi đè NODE_OPTIONS trên prototype, dẫn đến thực thi lệnh tùy ý.
Phát hiện và ngăn chặn
Với CSPT (phía front-end):
- Sanitize tất cả user input trước khi dùng làm URL path parameter - loại bỏ ký tự
/, chuỗi../, và các dạng URL-encoded tương đương - Sử dụng Doyensec CSPT Burp Extension để scan passive proxy history
- Review code tập trung vào pattern:
URLSearchParams.get()- >fetch()/axios()mà không có sanitize ở giữa
Với prototype chain bypass (phía back-end):
- Thay
obj[key]bằngObject.hasOwn(obj, key)cho mọi critical lookup (OTP, permissions, feature flags) - Dùng
Mapthay plain object cho OTP store và session management - Validate JSON schema chặt - reject request có extra parameters ngoài spec đã định nghĩa
- Không nhận query parameter như là request body substitute cho PUT/PATCH endpoints
Timeline & nguồn
- Pre-2021: CSPT lần đầu ghi nhận trong Facebook bug bounty bởi Philippe Harewood
- 2021: Sam Curry tweet + USENIX Security 2021 paper đưa CSPT ra cộng đồng rộng hơn
- 2022: CSS Injection via CSPT vào PortSwigger Top 10 Web Hacking Techniques
- 2023: Doyensec công bố CSPT2CSRF whitepaper; CVE-2023-45316 và CVE-2023-6458 (Mattermost) được tiết lộ
- Tháng 4/2026: Writeup $15k - CSPT kết hợp prototype chain 2FA bypass thành full account takeover
Nguồn: whoareme.com - $15k writeup, Doyensec CSPT2CSRF Whitepaper, PortSwigger Web Security Academy.



