Giảm thiểu tập lệnh trên nhiều trang web (XSS) bằng một Chính sách bảo mật nội dung (CSP) nghiêm ngặt

Lukas Weichselbaum
Lukas Weichselbaum

Hỗ trợ trình duyệt

  • Chrome: 52.
  • Cạnh: 79.
  • Firefox: 52.
  • Safari: 15.4.

Nguồn

Tập lệnh trên nhiều trang web (XSS), khả năng chèn các tập lệnh độc hại vào một ứng dụng web, là một trong những khả năng lỗ hổng bảo mật web lớn nhất trong hơn một thập kỷ qua.

Chính sách bảo mật nội dung (CSP) là một lớp bảo mật bổ sung giúp giảm thiểu XSS. Để định cấu hình CSP, thêm tiêu đề HTTP Content-Security-Policy vào một trang web và thiết lập các giá trị kiểm soát những tài nguyên mà tác nhân người dùng có thể tải cho trang đó.

Trang này giải thích cách sử dụng CSP dựa trên nonces hoặc hashes để giảm thiểu XSS, thay vì những CSP dựa trên danh sách cho phép của máy chủ thường được dùng thường rời khỏi trang được hiển thị với XSS vì chúng có thể được bỏ qua trong hầu hết các cấu hình.

Từ khoá: Số chỉ dùng một lần là một số ngẫu nhiên chỉ được sử dụng một lần mà bạn có thể sử dụng để đánh dấu Thẻ <script> là đáng tin cậy.

Từ khoá: hàm băm là một hàm toán học chuyển đổi một giá trị đầu vào vào một giá trị số nén được gọi là hàm băm. Bạn có thể dùng hàm băm (ví dụ: SHA-256) để đánh dấu nội tuyến Thẻ <script> là đáng tin cậy.

Chính sách bảo mật nội dung dựa trên nonces hoặc hashes của nó thường được gọi là CSP nghiêm ngặt. Khi một ứng dụng dùng một CSP nghiêm ngặt, những kẻ tấn công tìm thấy HTML lỗi chèn thường không thể sử dụng chúng để buộc trình duyệt thực thi các tập lệnh độc hại trong tài liệu dễ bị tấn công. Lý do là vì chỉ Chính sách bảo mật nội dung (CSP) nghiêm ngặt cho phép tập lệnh hoặc tập lệnh đã băm có giá trị số chỉ dùng một lần chính xác được tạo trên để kẻ tấn công không thể thực thi tập lệnh nếu không biết chính xác số chỉ dùng một lần cho một câu trả lời cụ thể.

Vì sao bạn nên sử dụng một CSP nghiêm ngặt?

Nếu trang web của bạn đã có CSP giống như script-src www.googleapis.com, cách làm đó có thể không hiệu quả khi áp dụng trên nhiều trang web. Loại CSP này được gọi là CSP trong danh sách cho phép. Chúng đòi hỏi nhiều tuỳ chỉnh và có thể vượt qua.

Các CSP nghiêm ngặt dựa trên số chỉ dùng một lần hoặc hàm băm được mã hoá sẽ giúp tránh được những lỗi này.

Cấu trúc CSP nghiêm ngặt

Một Chính sách bảo mật nội dung nghiêm ngặt, cơ bản sử dụng một trong các phản hồi HTTP sau đây tiêu đề:

Chính sách bảo mật nội dung (CSP) nghiêm ngặt dựa trên số chỉ dùng một lần

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Cách hoạt động của CSP nghiêm ngặt dựa trên số chỉ dùng một lần.

Chính sách bảo mật nội dung (CSP) nghiêm ngặt dựa trên hàm băm

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Những tài sản sau đây đặt một CSP như thế này là "nghiêm ngặt" và do đó bảo mật:

  • Hàm này sử dụng nonces 'nonce-{RANDOM}' hoặc băm 'sha256-{HASHED_INLINE_SCRIPT}' để cho biết nhà phát triển trên trang web tin tưởng những thẻ <script> nào sẽ thực thi trình duyệt của người dùng.
  • Chế độ này sẽ thiết lập 'strict-dynamic' giúp giảm công sức triển khai CSP số chỉ dùng một lần hoặc dựa trên hàm băm bằng cách tự động cho phép thực thi các tập lệnh mà một tập lệnh đáng tin cậy tạo ra. Việc này cũng bỏ chặn việc sử dụng hầu hết các thư viện và tiện ích JavaScript của bên thứ ba.
  • URL này không dựa trên danh sách cho phép về URL nên không bị những cách bỏ qua CSP phổ biến.
  • Thao tác này chặn các tập lệnh cùng dòng không đáng tin cậy như trình xử lý sự kiện cùng dòng hoặc javascript: URI.
  • Trình bổ trợ này hạn chế object-src để vô hiệu hoá các trình bổ trợ nguy hiểm như Flash.
  • Chính sách này hạn chế base-uri để chặn việc chèn thẻ <base>. Điều này giúp ngăn chặn kẻ tấn công thay đổi vị trí của các tập lệnh đã tải từ các URL tương đối.

Áp dụng CSP nghiêm ngặt

Để áp dụng một CSP nghiêm ngặt, bạn cần phải:

  1. Quyết định xem ứng dụng của bạn nên đặt CSP dựa trên số chỉ dùng một lần hay dựa trên hàm băm.
  2. Sao chép CSP từ mục Cấu trúc CSP nghiêm ngặt rồi thiết lập làm tiêu đề phản hồi trên ứng dụng.
  3. Tái cấu trúc mẫu HTML và mã phía máy khách để xoá các mẫu không tương thích với CSP.
  4. Triển khai CSP.

Bạn có thể sử dụng Lighthouse (phiên bản 7.3.0 trở lên có cờ --preset=experimental) Kiểm tra Các phương pháp hay nhất trong suốt quá trình này để kiểm tra xem trang web của bạn có CSP hay không và đủ nghiêm ngặt để chống lại XSS.

Ngọn hải đăng
  cảnh báo cho biết không phát hiện thấy CSP nào ở chế độ thực thi.
Nếu trang web của bạn không có CSP, Lighthouse sẽ hiển thị cảnh báo này.

Bước 1: Quyết định xem bạn cần CSP dựa trên số chỉ dùng một lần hay CSP dựa trên hàm băm

Sau đây là cách hoạt động của 2 loại CSP nghiêm ngặt:

CSP dựa trên số chỉ dùng một lần

Với CSP số chỉ dùng một lần, bạn sẽ tạo một số ngẫu nhiên trong thời gian chạy rồi đưa số đó vào CSP của bạn và liên kết CSP đó với mọi thẻ tập lệnh trên trang của bạn. Kẻ tấn công không thể bao gồm hoặc chạy tập lệnh độc hại trong trang của bạn, bởi vì chúng sẽ cần đoán số ngẫu nhiên chính xác cho tập lệnh đó. Phương thức này chỉ hiệu quả nếu số điện thoại không thể đoán được và mới được tạo trong thời gian chạy cho mỗi phản hồi.

Sử dụng CSP dựa trên số chỉ dùng một lần cho các trang HTML hiển thị trên máy chủ. Đối với các trang này, bạn có thể tạo một số ngẫu nhiên mới cho mỗi câu trả lời.

CSP dựa trên hàm băm

Đối với CSP dựa trên hàm băm, hàm băm của mọi thẻ tập lệnh cùng dòng sẽ được thêm vào CSP đó. Mỗi tập lệnh có một hàm băm khác nhau. Kẻ tấn công không thể đưa vào hoặc chạy phần mềm độc hại trong trang của bạn, vì hàm băm của tập lệnh đó cần phải nằm trong tập lệnh CSP cần chạy.

Dùng một CSP dựa trên hàm băm cho các trang HTML được phân phát tĩnh hoặc các trang cần được đã lưu vào bộ nhớ đệm. Ví dụ: bạn có thể dùng CSP dựa trên hàm băm cho trang web một trang ứng dụng được xây dựng bằng các khung như Angular, React hoặc các khung khác được phân phát tĩnh mà không cần hiển thị phía máy chủ.

Bước 2: Thiết lập một CSP nghiêm ngặt và chuẩn bị kịch bản

Khi thiết lập CSP, bạn có một số lựa chọn như sau:

  • Chế độ chỉ báo cáo (Content-Security-Policy-Report-Only) hoặc chế độ thực thi (Content-Security-Policy). Ở chế độ chỉ báo cáo, CSP sẽ không chặn vì vậy, không có gì trên trang web của bạn bị lỗi, nhưng bạn có thể thấy lỗi và các báo cáo cho mọi nội dung lẽ ra đã bị chặn. Tại địa phương, khi bạn việc đặt CSP của mình không thực sự quan trọng, vì cả hai chế độ đều cho bạn biết trong bảng điều khiển trình duyệt. Chế độ thực thi có thể giúp bạn tìm ra tài nguyên cho CSP dự thảo của bạn chặn, vì việc chặn một tài nguyên có thể khiến giao diện trang bị hỏng. Chế độ chỉ báo cáo sẽ hữu ích nhất ở giai đoạn sau của quá trình này (xem Bước 5).
  • Tiêu đề hoặc thẻ HTML <meta>. Để phát triển cục bộ, thẻ <meta> có thể được thuận tiện cho việc tinh chỉnh CSP và nhanh chóng nhận thấy CSP ảnh hưởng như thế nào đến trang web của bạn. Tuy nhiên:
    • Sau này, khi triển khai CSP trong thực tế, bạn nên đặt nó làm tiêu đề HTTP.
    • Nếu muốn đặt CSP của mình ở chế độ chỉ báo cáo, bạn cần phải đặt CSP đó làm vì thẻ meta CSP không hỗ trợ chế độ chỉ báo cáo.

Cách A: CSP dựa trên số chỉ dùng một lần

Đặt phản hồi HTTP Content-Security-Policy sau đây trong ứng dụng của bạn:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Tạo một số chỉ dùng một lần cho CSP

Số chỉ dùng một lần là một số ngẫu nhiên chỉ được sử dụng một lần cho mỗi lượt tải trang. Dựa trên số chỉ dùng một lần CSP chỉ có thể giảm thiểu XSS nếu kẻ tấn công không thể đoán được giá trị số chỉ dùng một lần. Đáp Số chỉ dùng một lần cho CSP phải:

  • Một giá trị ngẫu nhiên được mã hoá mạnh (lý tưởng là có độ dài từ 128 bit trở lên)
  • Mới được tạo cho mỗi câu trả lời
  • Được mã hoá Base64

Dưới đây là một số ví dụ về cách thêm số chỉ dùng một lần CSP trong khung phía máy chủ:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

Thêm thuộc tính nonce vào các phần tử <script>

Với CSP dựa trên số chỉ dùng một lần, mọi phần tử <script> đều phải có thuộc tính nonce khớp với số chỉ dùng một lần ngẫu nhiên được chỉ định trong tiêu đề CSP. Tất cả tập lệnh đều có thể có cùng một tập lệnh số chỉ dùng một lần. Bước đầu tiên là thêm các thuộc tính này vào tất cả tập lệnh để CSP cho phép họ.

Lựa chọn B: Tiêu đề phản hồi của CSP dựa trên hàm băm

Đặt phản hồi HTTP Content-Security-Policy sau đây trong ứng dụng của bạn:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Đối với nhiều tập lệnh cùng dòng, cú pháp như sau: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Tải tập lệnh được nguồn một cách linh động

Vì hàm băm CSP chỉ được hỗ trợ trên các trình duyệt cho tập lệnh cùng dòng, bạn phải tải tất cả tập lệnh của bên thứ ba một cách linh động bằng cách sử dụng tập lệnh cùng dòng. Hàm băm cho tập lệnh được lấy nguồn không được hỗ trợ tốt trên các trình duyệt.

Ví dụ về cách đưa tập lệnh vào cùng dòng.
Do CSP cho phép
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
Để tập lệnh này chạy, bạn phải tính toán hàm băm của tập lệnh cùng dòng rồi thêm thông tin đó vào tiêu đề phản hồi CSP, thay thế {HASHED_INLINE_SCRIPT} phần giữ chỗ. Để giảm số lượng hàm băm, bạn có thể hợp nhất tất cả cùng dòng thành một tập lệnh duy nhất. Để xem ví dụ thực tế, hãy tham khảo ví dụ của tài khoản đó.
Bị chặn bởi CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP chặn các tập lệnh này vì hệ thống chỉ băm được các tập lệnh cùng dòng.

Những điều cần cân nhắc khi tải tập lệnh

Ví dụ về tập lệnh cùng dòng sẽ thêm s.async = false để đảm bảo foo thực thi trước bar, ngay cả khi bar sẽ tải trước. Trong đoạn mã này, s.async = false không chặn trình phân tích cú pháp trong khi tải tập lệnh vì các tập lệnh được thêm một cách linh động. Trình phân tích cú pháp chỉ dừng khi thực thi tập lệnh, như cho async tập lệnh. Tuy nhiên, với đoạn mã này, lưu ý:

  • Một hoặc cả hai tập lệnh có thể thực thi trước khi tài liệu hoàn tất đang tải xuống. Nếu bạn muốn tài liệu sẵn sàng vào thời điểm các tập lệnh sẽ thực thi, hãy đợi sự kiện DOMContentLoaded rồi bạn nối thêm các tập lệnh. Nếu việc này gây ra vấn đề về hiệu suất vì tập lệnh không bắt đầu tải xuống đủ sớm, hãy sử dụng tải trước thẻ sớm hơn trên trang.
  • defer = true không làm gì cả. Nếu bạn cần hãy chạy tập lệnh theo cách thủ công khi cần.

Bước 3: Tái cấu trúc mẫu HTML và mã phía máy khách

Trình xử lý sự kiện cùng dòng (chẳng hạn như onclick="…", onerror="…") và URI JavaScript Có thể dùng (<a href="javascript:…">) để chạy tập lệnh. Điều này có nghĩa là khi tìm thấy lỗi XSS có thể chèn loại HTML này và thực thi mã độc hại JavaScript. CSP dựa trên số chỉ dùng một lần hoặc dựa trên hàm băm nghiêm cấm sử dụng loại mã đánh dấu này. Nếu trang web của bạn sử dụng bất kỳ mẫu nào trong số này, bạn sẽ cần phải tái cấu trúc chúng sao cho an toàn hơn lựa chọn thay thế.

Nếu đã bật CSP ở bước trước, bạn sẽ có thể xem các trường hợp vi phạm CSP trong bảng điều khiển mỗi khi CSP chặn một mẫu không tương thích.

Báo cáo vi phạm CSP trong Bảng điều khiển dành cho nhà phát triển Chrome.
Lỗi trên bảng điều khiển liên quan đến mã bị chặn.

Trong hầu hết các trường hợp, cách khắc phục rất đơn giản:

Tái cấu trúc trình xử lý sự kiện cùng dòng

Do CSP cho phép
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
CSP cho phép các trình xử lý sự kiện được đăng ký bằng JavaScript.
Bị chặn bởi CSP
<span onclick="doThings();">A thing.</span>
CSP chặn các trình xử lý sự kiện cùng dòng.

Tái cấu trúc URI javascript:

Do CSP cho phép
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP cho phép các trình xử lý sự kiện được đăng ký bằng JavaScript.
Bị chặn bởi CSP
<a href="javascript:linkClicked()">foo</a>
CSP chặn javascript: URI.

Xoá eval() khỏi JavaScript của bạn

Nếu ứng dụng của bạn dùng eval() để chuyển đổi quá trình chuyển đổi tuần tự chuỗi JSON thành JS bạn nên tái cấu trúc các thực thể đó thành JSON.parse(). Phương thức này cũng nhanh hơn.

Nếu không thể xoá tất cả các trường hợp sử dụng eval(), bạn vẫn có thể đặt một mức nghiêm ngặt dựa trên số chỉ dùng một lần CSP, nhưng bạn phải sử dụng từ khoá CSP 'unsafe-eval', điều này khiến chính sách kém an toàn hơn một chút.

Bạn có thể tìm thấy những ví dụ này và nhiều ví dụ khác về hoạt động tái cấu trúc trong CSP nghiêm ngặt này codelab:

Bước 4 (Không bắt buộc): Thêm tính năng dự phòng để hỗ trợ các phiên bản trình duyệt cũ

Hỗ trợ trình duyệt

  • Chrome: 52.
  • Cạnh: 79.
  • Firefox: 52.
  • Safari: 15.4.

Nguồn

Nếu bạn cần hỗ trợ các phiên bản trình duyệt cũ:

  • Để sử dụng strict-dynamic, bạn phải thêm https: làm phương án dự phòng cho lần sử dụng trước đó của Safari. Khi bạn thực hiện việc này:
    • Tất cả trình duyệt hỗ trợ strict-dynamic bỏ qua phương thức dự phòng https:, để không làm giảm sức mạnh của chính sách.
    • Trong các trình duyệt cũ, các tập lệnh tìm nguồn bên ngoài chỉ có thể tải nếu các tập lệnh đó đến từ nguồn gốc HTTPS. Chế độ này kém an toàn hơn so với một CSP nghiêm ngặt, nhưng vẫn ngăn chặn một số nguyên nhân thường gặp về XSS, chẳng hạn như chèn URI javascript:.
  • Để đảm bảo khả năng tương thích với các phiên bản trình duyệt rất cũ (từ 4 năm trở lên), bạn có thể thêm unsafe-inline làm phương án dự phòng. Tất cả trình duyệt gần đây bỏ qua unsafe-inline nếu có số chỉ dùng một lần hoặc hàm băm CSP.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Bước 5: Triển khai CSP

Sau khi xác nhận rằng CSP của bạn không chặn bất kỳ tập lệnh hợp lệ nào trong môi trường phát triển cục bộ, bạn có thể triển khai CSP của mình vào giai đoạn thử nghiệm, sau đó đến môi trường sản xuất:

  1. (Không bắt buộc) Triển khai CSP của bạn ở chế độ chỉ báo cáo bằng cách sử dụng Tiêu đề Content-Security-Policy-Report-Only. Chế độ chỉ báo cáo rất hữu ích thử nghiệm một thay đổi có thể gây lỗi như một CSP mới trong phiên bản chính thức trước khi bạn bắt đầu thực thi các quy định hạn chế về CSP. Ở chế độ chỉ báo cáo, CSP của bạn không ảnh hưởng đến hành vi của ứng dụng, nhưng trình duyệt vẫn tạo lỗi trên bảng điều khiển và báo cáo vi phạm khi gặp các mẫu không tương thích với CSP của bạn, để bạn có thể biết được điều gì sẽ gây ra lỗi cho người dùng cuối. Để biết thêm hãy xem bài viết về API báo cáo.
  2. Khi bạn tin rằng CSP của mình không phá vỡ trang web cho người dùng cuối, triển khai CSP bằng cách sử dụng tiêu đề phản hồi Content-Security-Policy. T4 bạn nên đặt CSP của mình bằng cách sử dụng tiêu đề HTTP phía máy chủ vì đây là cách an toàn hơn thẻ <meta>. Sau khi bạn hoàn tất bước này, CSP của bạn sẽ bắt đầu bảo vệ ứng dụng của bạn khỏi XSS.

Các điểm hạn chế

Một CSP nghiêm ngặt thường cung cấp một lớp bảo mật bổ sung mạnh mẽ để giúp giảm thiểu XSS. Trong hầu hết các trường hợp, CSP làm giảm đáng kể bề mặt tấn công, bằng cách từ chối các mẫu nguy hiểm như URI javascript:. Tuy nhiên, tuỳ theo loại của CSP mà bạn đang sử dụng (số chỉ dùng một lần, hàm băm, có hoặc không có 'strict-dynamic'), ở đó là những trường hợp mà CSP cũng không bảo vệ ứng dụng của bạn:

  • Nếu bạn chỉ dùng một tập lệnh nhưng có một lệnh chèn trực tiếp vào phần nội dung hoặc mã Tham số src của phần tử <script> đó.
  • Nếu có chèn vào vị trí của các tập lệnh được tạo động (document.createElement('script')), bao gồm cả các hàm trong thư viện tạo các nút DOM script dựa trên giá trị của đối số. Chiến dịch này bao gồm một số API phổ biến như .html() của jQuery, cũng như .get().post() trong jQuery < 3,0.
  • Nếu có chèn mẫu trong các ứng dụng AngularJS cũ. Kẻ tấn công có thể chèn vào mẫu AngularJS có thể sử dụng nó để thực thi JavaScript tuỳ ý.
  • Nếu chính sách này chứa 'unsafe-eval', sẽ chèn vào eval(), setTimeout() và một vài API khác ít khi sử dụng.

Các nhà phát triển và kỹ sư bảo mật cần đặc biệt chú ý đến mẫu trong quá trình xem xét mã và kiểm tra bảo mật. Bạn có thể xem thêm chi tiết trên những trường hợp này trong Chính sách bảo mật nội dung: Sự hỗn loạn thành công giữa tăng cường và giảm thiểu.

Tài liệu đọc thêm