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.

CSPT account takeover va 2FA bypass qua prototype chain - $15k bug bounty

Bốn bước từ invite link đến full access

Toàn bộ chuỗi tấn công diễn ra như sau:

  1. 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.
  2. 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.
  3. 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 OK và cấp session token.
  4. 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 teamIdinviteId 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=a
  • inviteId = ../

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:

  1. pendingCodes["__proto__"] không có own key nào tên là __proto__
  2. JavaScript leo lên prototype chain theo thuật toán [[Get]] của ECMAScript
  3. __proto__ là accessor property trên Object.prototype, getter trả về chính Object.prototype
  4. Object.prototype là một object - luôn truthy
  5. if (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 chain
  • Reflect.has(obj, key) - tương đương in
  • obj[key] !== undefined - vẫn invoke [[Get]]

Fix đúng cách:

  • Object.hasOwn(pendingCodes, code) - chỉ check own properties (ES2022)
  • Dùng Map thay plain object - Map dù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ẩmCVETác động tệ nhất
MattermostCVE-2023-45316RCE qua install malicious plugin
MattermostCVE-2023-64581-click POST CSRF không cần tương tác
Rocket.Chat-1-click logout CSRF
GitLab-1-click CSRF
Facebook-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ằng Object.hasOwn(obj, key) cho mọi critical lookup (OTP, permissions, feature flags)
  • Dùng Map thay 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.