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.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Nguồn

Tập lệnh trên nhiều trang web (XSS), khả năng chèn tập lệnh độc hại vào ứng dụng web, là một trong nhữ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, hãy thêm tiêu đề HTTP Content-Security-Policy vào một trang web và đặt các giá trị kiểm soát 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 số chỉ dùng một lần hoặc hàm băm để giảm thiểu XSS, thay vì các CSP dựa trên danh sách cho phép của máy chủ thường dùng thường khiến trang hiển thị với XSS vì chúng có thể bỏ qua trong hầu hết các cấu hình.

Thuật ngữ chính: Số chỉ dùng một lần (nonce) là một số ngẫu nhiên chỉ được dùng một lần mà bạn có thể dùng để đánh dấu thẻ <script> là đáng tin cậy.

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

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

Tại sao bạn nên sử dụng CSP nghiêm ngặt?

Nếu trang web của bạn đã có một CSP có dạng như script-src www.googleapis.com, thì có thể CSP đó không hiệu quả đối với các cuộc tấn công trên nhiều trang web. Loại CSP này được gọi là CSP danh sách cho phép. Các trình bảo vệ này yêu cầu nhiều tuỳ chỉnh và có thể bị kẻ tấn công bỏ 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 mã hoá giúp tránh được những cạm bẫy 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 tiêu đề phản hồi HTTP sau:

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';

Sau đây là những thuộc tính khiến CSP như thế này trở nên "nghiêm ngặt" và do đó bảo mật:

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

Sử 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 có nên đặt CSP dựa trên số chỉ dùng một lần hay hàm băm hay không.
  2. Sao chép CSP từ phần Cấu trúc CSP nghiêm ngặt và đặt CSP đó làm tiêu đề phản hồi trên ứng dụng.
  3. Tái cấu trúc cá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 tính năng kiểm tra Các phương pháp hay nhất của Lighthouse (phiên bản 7.3.0 trở lên có cờ --preset=experimental) trong suốt quá trình này để kiểm tra xem trang web của bạn có CSP hay không và liệu CSP đó có đủ nghiêm ngặt để chống lại XSS hay không.

Báo cáo Lighthouse cảnh báo rằng không tìm 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ó cần CSP dựa trên số chỉ dùng một lần hay hàm băm không

Dưới đây là cách hoạt động của hai loại CSP nghiêm ngặt:

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

Với CSP dựa trên 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, đưa số này vào CSP và liên kết số này với mọi thẻ tập lệnh trong trang. Kẻ tấn công không thể đưa hoặc chạy tập lệnh độc hại trong trang của bạn vì chúng cần phải đoán chính xác số ngẫu nhiên cho tập lệnh đó. Điều này chỉ hoạt động nếu số này không thể đoán được và được tạo mới 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 phản hồ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 nội tuyến 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 hoặc chạy một tập lệnh độc hại vào trang của bạn, vì hàm băm của tập lệnh đó cần phải nằm trong CSP để có thể chạy.

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

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 dùng một lần cho mỗi lượt tải trang. CSP dựa trên số chỉ dùng một lần 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. Số chỉ dùng một lần của 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

Sau đây là một số ví dụ về cách thêm số chỉ dùng một lần CSP trong các 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 phần tử <script>

Với CSP số chỉ dùng một lần dựa trên số chỉ dùng một lần, mỗi phần tử <script> phải có thuộc tính nonce khớp với giá trị 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 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 các tập lệnh đó.

Cách B: Tiêu đề phản hồi CSP dựa trên hàm băm

Đặt tiêu đề phản hồi HTTP Content-Security-Policy sau đây trong ứng dụng:

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 nội tuyến, cú pháp sẽ như sau: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

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

Bạn có thể tải tập lệnh của bên thứ ba một cách linh động bằng tập lệnh nội tuyến.

Ví dụ về cách nội tuyến tập lệnh.
Được 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>
Để cho phép tập lệnh này chạy, bạn phải tính hàm băm của tập lệnh nội tuyến và thêm hàm băm đó vào tiêu đề phản hồi CSP, thay thế phần giữ chỗ {HASHED_INLINE_SCRIPT}. Để giảm số lượng hàm băm, bạn có thể hợp nhất tất cả tập lệnh cùng dòng thành một tập lệnh duy nhất. Để xem cách thực hiện, hãy tham khảo ví dụ này và của ví dụ.
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ì chúng không được thêm một cách linh động và không có thuộc tính integrity khớp với nguồn được phép.

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

Ví dụ về tập lệnh nội tuyến sẽ thêm s.async = false để đảm bảo rằng foo thực thi trước bar, ngay cả khi bar 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 các tập lệnh tải, 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 trong khi các tập lệnh thực thi, như đối với các tập lệnh async. Tuy nhiên, với đoạn mã này, hã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 tải xuống xong. Nếu bạn muốn tài liệu sẵn sàng khi các tập lệnh thực thi, hãy đợi sự kiện DOMContentLoaded trước khi thêm các tập lệnh. Nếu việc này gây ra vấn đề về hiệu suất do các tập lệnh không bắt đầu tải xuống sớm, hãy sử dụng thẻ tải trước sớm hơn trên trang.
  • defer = true không làm gì cả. Nếu bạn cần hành vi đó, 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

Bạn có thể dùng trình xử lý sự kiện nội tuyến (chẳng hạn như onclick="…", onerror="…") và URI JavaScript (<a href="javascript:…">) để chạy tập lệnh. Điều này có nghĩa là một kẻ tấn công tìm thấy lỗi XSS có thể chèn loại HTML này và thực thi JavaScript độc hại. CSP dựa trên số chỉ dùng một lần hoặc hàm băm sẽ 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 cần phải tái cấu trúc các mẫu đó thành các giải pháp thay thế an toàn hơn.

Nếu đã bật CSP ở bước trước, bạn sẽ có thể xem các lỗi vi phạm về 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.
Các lỗi trên bảng điều khiển đối với 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

Được 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 theo CSP
<span onclick="doThings();">A thing.</span>
CSP chặn trình xử lý sự kiện nội tuyến.

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

Được 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

Nếu ứng dụng của bạn sử dụng eval() để chuyển đổi quá trình chuyển đổi tuần tự chuỗi JSON thành các đối tượng JS, thì bạn nên tái cấu trúc các thực thể đó thành JSON.parse() để 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 CSP dựa trên số chỉ dùng một lần nghiêm ngặt, nhưng bạn phải sử dụng từ khoá CSP 'unsafe-eval', điều này khiến chính sách của bạn 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ề việc tái cấu trúc như vậy trong lớp học lập trình CSP nghiêm ngặt này:

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

Hỗ trợ trình duyệt

  • Chrome: 52.
  • Edge: 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ũ hơn:

  • Để sử dụng strict-dynamic, bạn cần thêm https: làm phương án dự phòng cho các phiên bản Safari cũ. Khi bạn làm như vậy:
    • Tất cả trình duyệt hỗ trợ strict-dynamic đều bỏ qua tính năng dự phòng https: nên sẽ không làm giảm độ mạnh của chính sách này.
    • Trong các trình duyệt cũ, các tập lệnh có nguồn bên ngoài chỉ có thể tải nếu chúng đến từ một nguồn gốc HTTPS. Phương thức này kém an toàn hơn so với CSP nghiêm ngặt, nhưng vẫn ngăn chặn một số nguyên nhân phổ biến gây ra XSS 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 đều 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 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 cho môi trường thử nghiệm, sau đó triển khai cho môi trường phát hành chính thức:

  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 để kiểm thử một thay đổi có thể gây lỗi như một CSP mới trong môi trường phát hành công khai 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 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 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. Nhờ đó, bạn có thể xem những lỗi có thể xảy ra với người dùng cuối. Để biết thêm thông tin, hãy xem API báo cáo.
  2. Khi bạn chắc chắn rằng CSP sẽ không làm hỏng trang web của bạn đối với người dùng cuối, hãy triển khai CSP bằng tiêu đề phản hồi Content-Security-Policy. Bạn nên đặt CSP bằng tiêu đề HTTP phía máy chủ vì phương thức này bảo mật hơn thẻ <meta>. Sau khi bạn hoàn tất bước này, CSP 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 sẽ 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, dựa trên loại CSP 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'), có một số trường hợp CSP cũng không bảo vệ ứng dụng của bạn:

  • Nếu bạn tạo một tập lệnh một lần, nhưng có một hoạt động chèn trực tiếp vào nội dung hoặc thông số src của phần tử <script> đó.
  • Nếu có hoạt động 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ả vào bất kỳ hàm thư viện nào tạo các nút DOM script dựa trên giá trị của đối số. Danh sá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ó hoạt động chèn mẫu trong các ứng dụng AngularJS cũ. Kẻ tấn công có thể chèn vào một mẫu AngularJS có thể sử dụng mẫu này để thực thi JavaScript tuỳ ý.
  • Nếu chính sách chứa 'unsafe-eval', các lệnh chèn vào eval(), setTimeout() và một số API hiếm khi được sử dụng khác.

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

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