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 một ứ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ỷ.
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 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 một 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 không hiển thị với XSS vì chúng có thể bị 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ỉ dùng một lần và bạn có thể dùng số này để đánh dấu một 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 giá trị đầu vào thành giá trị số nén được gọi là hàm băm. Bạn có thể sử dụng hàm băm (ví dụ: SHA-256) để đánh dấu một thẻ <script>
cùng dòng là đáng tin cậy.
Chính sách bảo mật nội dung dựa trên nonces hoặc hashes thường được gọi là CSP nghiêm ngặt. Khi một ứng dụng sử dụng một CSP nghiêm ngặt, những kẻ tấn công tìm thấy lỗi chèn HTML 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 một tài liệu dễ bị tấn công. Nguyên nhân là do CSP nghiêm ngặt chỉ cho phép các tập lệnh băm hoặc tập lệnh có đúng giá trị số chỉ dùng một lần đượ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 chính xác số chỉ dùng một lần cho một phản hồi nhất định.
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
, thì CSP đó có thể không hiệu quả trên nhiều trang web. Loại CSP này được gọi là CSP trong danh sách cho phép. Các đường liên kết này đòi hỏi phải tuỳ chỉnh rất nhiều và có thể bị những kẻ tấn công bỏ qua.
Các CSP nghiêm ngặt dựa trên nonces hoặc hashes được mã hoá sẽ tránh được những sai lầm này.
Cấu trúc CSP nghiêm ngặ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';
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';
Các thuộc tính sau tạo nên CSP như thế này "nghiêm ngặt" nên có tính bảo mật:
- Phương thức này sử dụng nonces
'nonce-{RANDOM}'
hoặc hàm băm'sha256-{HASHED_INLINE_SCRIPT}'
để cho biết những thẻ<script>
mà nhà phát triển của trang web tin tưởng sẽ thực thi trong trình duyệt của người dùng. - Chính sách này đặt
'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. Thao tá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 URL, vì vậy, URL này không bị bỏ qua CSP thường gặp.
- Tính năng 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:
. - Chính sách này hạn chế
object-src
để tắt các trình bổ trợ nguy hiểm như Flash. - Chính sách này hạn chế
base-uri
để chặn hành vi chèn thẻ<base>
. Điều này ngăn 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:
- Quyết định xem ứng dụng của bạn nên đặt CSP số chỉ dùng một lần hay CSP dựa trên hàm băm.
- Sao chép CSP từ mục cấu trúc CSP nghiêm ngặt và đặt nó làm tiêu đề phản hồi trên ứng dụng của bạn.
- 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.
- Triển khai CSP.
Bạn có thể sử dụng Lighthouse (phiên bản 7.3.0 trở lên với cờ --preset=experimental
) trong suốt quy trình kiểm tra Các phương pháp hay nhất để kiểm tra xem trang web của bạn có CSP hay không và liệu trang web đó có đủ nghiêm ngặt để chống lại XSS hay không.
Bước 1: Quyết định xem bạn cần CSP số chỉ dùng một lần hay CSP dựa trên hàm băm
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ố đó vào CSP và liên kết số đó với mọi thẻ tập lệnh trên trang của bạn. Kẻ tấn công không thể đưa vào hoặc chạy tập lệnh độc hại trên trang của bạn, vì chúng cần đoán đúng số ngẫu nhiên cho tập lệnh đó. Cách này chỉ hiệu quả 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 được 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 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ể thêm hoặc chạy tập lệnh độ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 CSP của bạn để tập lệnh 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 trang đơn đượ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: Đặt một CSP nghiêm ngặt và chuẩn bị tập lệnh
Sau đây là một số cách khi thiết lập CSP:
- 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 nên sẽ 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à nhận báo cáo về mọi nội dung có thể đã bị chặn. Trên máy, khi bạn thiết lập CSP của mình, điều này không thực sự quan trọng, vì cả hai chế độ đều cho bạn thấy các lỗi trong bảng điều khiển của 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à CSP nháp của bạn chặn, vì việc chặn tài nguyên có thể khiến trang của bạn trông bị hỏng. Chế độ chỉ báo cáo sẽ trở nên hữu ích nhất trong quá trình này (xem Bước 5). - Thẻ tiêu đề hoặc thẻ HTML
<meta>
. Đối với việc 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à xem nhanh mức độ ảnh hưởng của CSP đó đến trang web của bạn. Tuy nhiên:- Sau này, khi triển khai CSP trong phiên bản chính thức, bạn nên đặt CSP này 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 đặt CSP đó làm tiêu đề, vì thẻ meta CSP không hỗ trợ chế độ chỉ báo cáo.
Đặt tiêu đề 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 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 đ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:
- Giá trị ngẫu nhiên mạnh được mã hoá (tốt nhất là dài từ 128 bit trở lên)
- Được tạo mới 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 vào khung phía máy chủ:
- Django (Python)
- Sao lưu nhanh (JavaScript):
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>
phải có một thuộc tính nonce
khớp với giá trị số chỉ dùng một lần được chỉ định trong tiêu đề CSP. Tất cả tập lệnh có thể có cùng 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 mọi tập lệnh để CSP cho phép các thuộc tính đó.
Đặt tiêu đề 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 sẽ như sau:
'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'
.
Tải tập lệnh 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 đối với các tập lệnh cùng dòng, nên bạn phải tự động tải tất cả các tập lệnh của bên thứ ba bằng cách sử dụng một tập lệnh cùng dòng. Hàm băm cho các tập lệnh nguồn không được hỗ trợ tốt trên các trình duyệt.
<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>
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
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 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 tập lệnh tải, vì các tập lệnh được thêm tự động. Trình phân tích cú pháp chỉ dừng lại khi các tập lệnh thực thi, giống như đối với tập lệnh async
. Tuy nhiên, đối 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 xuống xong tài liệu. Nếu bạn muốn tài liệu sẵn sàng vào thời điểm
thực thi tập lệnh, hãy đợi sự kiện
DOMContentLoaded
trước khi thêm tập lệnh. Nếu việc này gây ra vấn đề về hiệu suất do tập lệnh không bắt đầu tải xuống sớm, hãy sử dụng tải trước các thẻ 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 các trình xử lý sự kiện cùng dòng (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à 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 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 cần tái cấu trúc chúng 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ể thấy các lỗi 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.
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
<span id="things">A thing.</span> <script nonce="${nonce}"> document.getElementById('things').addEventListener('click', doThings); </script>
<span onclick="doThings();">A thing.</span>
Tái cấu trúc URI javascript:
<a id="foo">foo</a> <script nonce="${nonce}"> document.getElementById('foo').addEventListener('click', linkClicked); </script>
<a href="javascript:linkClicked()">foo</a>
Xoá eval()
khỏi JavaScript
Nếu ứng dụng của bạn sử dụng eval()
để chuyển đổi việc chuyển đổi tuần tự chuỗi JSON thành đối tượng JS, thì bạn nên tái cấu trúc các thực thể đó thành JSON.parse()
, nhờ đó cũng nhanh hơn.
Nếu không thể xoá mọi trường hợp sử dụng eval()
, thì bạn vẫn có thể đặt CSP nghiêm ngặt dựa trên số chỉ dùng một lần, nhưng bạn phải sử dụng từ khoá CSP 'unsafe-eval'
. Điều này sẽ làm cho chính sách của bạn kém an toàn hơn một chút.
Bạn có thể xem các ví dụ sau đâ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 bản dự phòng để hỗ trợ các phiên bản trình duyệt cũ
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 phải thêmhttps:
làm phương án dự phòng cho các phiên bản Safari trước đó. Khi bạn thực hiện việc này:- Tất cả trình duyệt hỗ trợ
strict-dynamic
đều bỏ quahttps:
dự phòng, vì vậy, điều này sẽ không làm giảm độ mạnh của chính sách. - 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 các tập lệnh đó đến từ một nguồn gốc HTTPS. Cá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 XSS phổ biến như chèn URI
javascript:
.
- Tất cả trình duyệt hỗ trợ
- Để đảm bảo khả năng tương thích với các phiên bản trình duyệt đã quá 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ả các trình duyệt gần đây đều bỏ quaunsafe-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 để thử nghiệm, sau đó đến môi trường sản xuất:
- (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 phù hợp để kiểm thử một thay đổi có thể gây lỗi như 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 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 phải các mẫu không tương thích với CSP. Nhờ đó, bạn có thể biết được những gì đã xảy ra với người dùng cuối của mình. Để biết thêm thông tin, hãy xem bài viết API Báo cáo. - Khi bạn đã chắc chắn rằng CSP sẽ không làm hỏng trang web của bạn cho người dùng cuối, hãy triển khai CSP của bạn bằng cách sử dụng tiêu đề phản hồi
Content-Security-Policy
. Bạn nên thiết lập CSP của mình bằng cách sử dụng tiêu đề HTTP phía máy chủ vì cách này an toàn hơn so với 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 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, dựa trên loại CSP mà bạn đang dùng (số chỉ dùng một lần, hàm băm, có hoặc không có 'strict-dynamic'
), có những trường hợp 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 sẽ có một lệnh chèn trực tiếp vào phần nội dung hoặc tham số
src
của phần tử<script>
đó. - Nếu có tính nă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ả nội dung chèn vào bất kỳ hàm thư viện nào tạo nút DOMscript
dựa trên giá trị của các đối số của chúng. Việc này bao gồm một số API phổ biến như.html()
của jQuery, cũng như.get()
và.post()
trong jQuery < 3.0. - Nếu có tính nă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 đó để thực thi JavaScript tuỳ ý.
- Nếu chính sách này chứa
'unsafe-eval'
, sẽ chèn vàoeval()
,setTimeout()
và một vài API hiếm khi dùng khác.
Các nhà phát triển và kỹ sư bảo mật nê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 Chính sách bảo mật nội dung: Mối quan hệ thành công giữa việc tăng cường và giảm thiểu tình trạng.
Tài liệu đọc thêm
- CSP đã ngừng hoạt động, CSP phát trực tiếp muôn năm! Vấn đề an toàn của danh sách trắng và tương lai của Chính sách bảo mật nội dung
- Người đánh giá CSP
- Hội thảo LocoMoco: Chính sách bảo mật nội dung – Mối lo ngại thành công giữa tăng cường và giảm thiểu rủi ro
- Buổi nói chuyện tại Google I/O: Bảo mật ứng dụng web bằng các tính năng của nền tảng hiện đại