Việc xây dựng trải nghiệm phong phú trên web hiện nay gần như không thể tránh khỏi việc lồng ghép các thành phần và nội dung mà bạn không thực sự kiểm soát được. Tiện ích của bên thứ ba có thể thúc đẩy mức độ tương tác và đóng vai trò quan trọng trong trải nghiệm tổng thể của người dùng. Đôi khi, nội dung do người dùng tạo còn quan trọng hơn nội dung gốc của trang web. Bạn không nên chọn không làm cả hai việc này, vì cả hai đều làm tăng nguy cơ Something Bad™ có thể xảy ra trên trang web của bạn. Mỗi tiện ích mà bạn nhúng (mọi quảng cáo, mọi tiện ích mạng xã hội) đều là một vectơ tấn công tiềm ẩn đối với những kẻ có ý định độc hại:
Chính sách bảo mật nội dung (CSP) có thể giảm thiểu các rủi ro liên quan đến cả hai loại nội dung này bằng cách cho phép bạn đưa các nguồn tập lệnh và nội dung đáng tin cậy cụ thể vào danh sách trắng. Đây là một bước tiến lớn theo đúng hướng, nhưng đáng chú ý là biện pháp bảo vệ mà hầu hết các lệnh CSP cung cấp là nhị phân: tài nguyên được cho phép hoặc không được cho phép. Đôi khi, bạn nên nói rằng "Tôi không chắc mình thực sự tin tưởng nguồn nội dung này, nhưng nó rất đẹp! Trình duyệt, vui lòng nhúng nội dung đó, nhưng đừng để nội dung đó làm hỏng trang web của tôi."
Quyền tối thiểu
Về cơ bản, chúng ta đang tìm kiếm một cơ chế cho phép chỉ cấp cho nội dung được nhúng cấp độ chức năng tối thiểu cần thiết để thực hiện công việc của nội dung đó. Nếu một tiện ích không cần bật lên một cửa sổ mới, thì việc xoá quyền truy cập vào window.open sẽ không gây hại. Nếu không yêu cầu Flash, bạn có thể tắt tính năng hỗ trợ trình bổ trợ mà không gặp vấn đề gì. Chúng ta có thể bảo mật tối đa nếu tuân thủ nguyên tắc đặc quyền tối thiểu và chặn mọi tính năng không liên quan trực tiếp đến chức năng mà chúng ta muốn sử dụng. Kết quả là chúng ta không còn phải tin tưởng một cách mù quáng rằng một số nội dung được nhúng sẽ không lợi dụng các đặc quyền mà nội dung đó không được sử dụng. Ứng dụng này đơn giản là sẽ không có quyền truy cập vào chức năng ngay từ đầu.
Các phần tử iframe
là bước đầu tiên hướng tới một khung tốt cho giải pháp như vậy.
Việc tải một số thành phần không đáng tin cậy trong iframe
sẽ giúp phân tách ứng dụng với nội dung bạn muốn tải. Nội dung được đóng khung sẽ không có quyền truy cập vào DOM của trang hoặc dữ liệu mà bạn đã lưu trữ cục bộ, cũng như không thể vẽ vào các vị trí tuỳ ý trên trang; nội dung này bị giới hạn trong phạm vi đường viền của khung. Tuy nhiên, việc phân tách này không thực sự hiệu quả. Trang được chứa vẫn có một số tuỳ chọn cho hành vi gây phiền toái hoặc độc hại: video tự động phát, trình bổ trợ và cửa sổ bật lên chỉ là một phần nhỏ.
Thuộc tính sandbox
của phần tử iframe
cung cấp cho chúng ta những thông tin cần thiết để thắt chặt các quy định hạn chế đối với nội dung được đóng khung. Chúng ta có thể hướng dẫn trình duyệt tải nội dung của một khung cụ thể trong môi trường có đặc quyền thấp, chỉ cho phép một tập hợp con các chức năng cần thiết để thực hiện mọi công việc cần làm.
Tin tưởng nhưng xác minh
Nút "Tweet" (Tin nhắn) của Twitter là một ví dụ tuyệt vời về chức năng có thể được nhúng một cách an toàn hơn trên trang web của bạn thông qua hộp cát. Twitter cho phép bạn nhúng nút này thông qua một iframe bằng mã sau:
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
Để tìm hiểu những gì chúng ta có thể khoá, hãy kiểm tra kỹ những chức năng mà nút này yêu cầu. HTML được tải vào khung thực thi một chút JavaScript từ máy chủ của Twitter và tạo một cửa sổ bật lên được điền sẵn giao diện tweet khi được nhấp vào. Giao diện đó cần có quyền truy cập vào cookie của Twitter để liên kết tweet với đúng tài khoản và cần có khả năng gửi biểu mẫu tweet. Đó là tất cả; khung không cần tải bất kỳ trình bổ trợ nào, không cần điều hướng cửa sổ cấp cao nhất hoặc bất kỳ chức năng nào khác. Vì không cần các đặc quyền đó, hãy xoá các đặc quyền đó bằng cách tạo hộp cát cho nội dung của khung.
Hộp cát hoạt động dựa trên danh sách cho phép. Chúng ta bắt đầu bằng cách xoá tất cả các quyền có thể, sau đó bật lại từng chức năng riêng lẻ bằng cách thêm các cờ cụ thể vào cấu hình của hộp cát. Đối với tiện ích Twitter, chúng tôi đã quyết định bật JavaScript, cửa sổ bật lên, tính năng gửi biểu mẫu và cookie của twitter.com. Chúng ta có thể thực hiện việc này bằng cách thêm thuộc tính sandbox
vào iframe
với giá trị sau:
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
Vậy là xong. Chúng tôi đã cung cấp cho khung tất cả các chức năng mà khung cần có và trình duyệt sẽ từ chối cấp quyền truy cập vào bất kỳ đặc quyền nào mà chúng tôi không cấp rõ ràng thông qua giá trị của thuộc tính sandbox
.
Kiểm soát chi tiết các chức năng
Chúng ta đã thấy một số cờ hộp cát có thể có trong ví dụ trên, giờ hãy cùng tìm hiểu chi tiết hơn về cách hoạt động bên trong của thuộc tính này.
Với một iframe có thuộc tính hộp cát trống, tài liệu được đóng khung sẽ được đưa vào hộp cát hoàn toàn, tuân theo các hạn chế sau:
- JavaScript sẽ không thực thi trong tài liệu được đóng khung. Điều này không chỉ bao gồm JavaScript được tải rõ ràng thông qua thẻ tập lệnh, mà còn bao gồm cả trình xử lý sự kiện nội tuyến và javascript: URL. Điều này cũng có nghĩa là nội dung chứa trong thẻ noscript sẽ hiển thị, giống như thể người dùng đã tự tắt tập lệnh.
- Tài liệu được đóng khung được tải vào một nguồn gốc duy nhất, nghĩa là tất cả các quy trình kiểm tra cùng nguồn gốc sẽ không thành công; các nguồn gốc duy nhất không bao giờ khớp với bất kỳ nguồn gốc nào khác, ngay cả với chính chúng. Trong số các tác động khác, điều này có nghĩa là tài liệu không có quyền truy cập vào dữ liệu được lưu trữ trong cookie của bất kỳ nguồn gốc nào hoặc bất kỳ cơ chế lưu trữ nào khác (bộ nhớ DOM, Cơ sở dữ liệu được lập chỉ mục, v.v.).
- Tài liệu được đóng khung không thể tạo cửa sổ hoặc hộp thoại mới (ví dụ: thông qua
window.open
hoặctarget="_blank"
). - Không thể gửi biểu mẫu.
- Trình bổ trợ sẽ không tải.
- Tài liệu được đóng khung chỉ có thể tự điều hướng chứ không phải điều hướng thành phần mẹ cấp cao nhất.
Việc đặt
window.top.location
sẽ gửi một ngoại lệ và việc nhấp vào đường liên kết bằngtarget="_top"
sẽ không có hiệu lực. - Các tính năng tự động kích hoạt (các thành phần biểu mẫu tự động lấy nét, video tự động phát, v.v.) sẽ bị chặn.
- Không thể lấy khoá con trỏ.
- Thuộc tính
seamless
bị bỏ qua trêniframes
mà tài liệu được đóng khung chứa.
Đây là một biện pháp nghiêm ngặt và tài liệu được tải vào iframe
được đặt trong hộp cát hoàn toàn thực sự rất ít rủi ro. Tất nhiên, sandbox cũng không thể làm được nhiều việc có giá trị: bạn có thể sử dụng sandbox đầy đủ cho một số nội dung tĩnh, nhưng hầu hết thời gian, bạn sẽ muốn nới lỏng một chút.
Ngoại trừ các trình bổ trợ, bạn có thể gỡ bỏ từng quy định hạn chế này bằng cách thêm cờ vào giá trị của thuộc tính hộp cát. Tài liệu trong hộp cát không bao giờ chạy được trình bổ trợ, vì trình bổ trợ là mã gốc không có hộp cát, nhưng mọi thứ khác đều là trò chơi công bằng:
allow-forms
cho phép gửi biểu mẫu.allow-popups
cho phép cửa sổ bật lên (đáng ngạc nhiên!).allow-pointer-lock
cho phép khoá con trỏ (thật bất ngờ!).allow-same-origin
cho phép tài liệu duy trì nguồn gốc; các trang được tải từhttps://example.com/
sẽ giữ lại quyền truy cập vào dữ liệu của nguồn gốc đó.allow-scripts
cho phép thực thi JavaScript và cũng cho phép các tính năng tự động kích hoạt (vì việc triển khai các tính năng này qua JavaScript sẽ rất đơn giản).allow-top-navigation
cho phép tài liệu thoát khỏi khung bằng cách điều hướng cửa sổ cấp cao nhất.
Với những điều này, chúng ta có thể đánh giá chính xác lý do tại sao chúng ta đã có một nhóm cờ hộp cát cụ thể trong ví dụ về Twitter ở trên:
- Bạn phải có
allow-scripts
vì trang được tải vào khung sẽ chạy một số JavaScript để xử lý hoạt động tương tác của người dùng. - Bạn phải có
allow-popups
vì nút này sẽ bật lên một biểu mẫu đăng tweet trong một cửa sổ mới. - Bạn phải có
allow-forms
vì biểu mẫu đăng tweet phải gửi được. - Bạn cần có
allow-same-origin
vì nếu không, bạn sẽ không thể truy cập vào cookie của twitter.com và người dùng sẽ không thể đăng biểu mẫu.
Một điều quan trọng cần lưu ý là cờ hộp cát áp dụng cho một khung cũng áp dụng cho mọi cửa sổ hoặc khung được tạo trong hộp cát. Điều này có nghĩa là chúng ta phải thêm allow-forms
vào hộp cát của khung, mặc dù biểu mẫu chỉ tồn tại trong cửa sổ mà khung bật lên.
Khi thuộc tính sandbox
được đặt, tiện ích chỉ nhận được các quyền cần thiết và các tính năng như trình bổ trợ, điều hướng trên cùng và khoá con trỏ vẫn bị chặn. Chúng tôi đã giảm nguy cơ nhúng tiện ích mà không gây ra tác động xấu.
Đây là một giải pháp có lợi cho tất cả các bên liên quan.
Phân tách đặc quyền
Việc tạo hộp cát cho nội dung của bên thứ ba để chạy mã không đáng tin cậy của họ trong môi trường có đặc quyền thấp là khá rõ ràng. Nhưng còn mã của riêng bạn thì sao? Bạn tin tưởng chính mình, phải không? Vậy tại sao bạn phải lo lắng về việc tạo hộp cát?
Tôi sẽ trả lời câu hỏi đó: nếu mã của bạn không cần trình bổ trợ, tại sao bạn lại cấp quyền truy cập vào trình bổ trợ? Tốt nhất là đây là một đặc quyền mà bạn không bao giờ sử dụng, tệ nhất là đây là một vectơ tiềm năng để kẻ tấn công xâm nhập. Mã của mọi người đều có lỗi và thực tế là mọi ứng dụng đều dễ bị khai thác theo cách này hay cách khác. Việc tạo hộp cát cho mã của riêng bạn có nghĩa là ngay cả khi kẻ tấn công xâm nhập thành công vào ứng dụng của bạn, chúng cũng sẽ không được cấp quyền truy cập đầy đủ vào nguồn gốc của ứng dụng; chúng sẽ chỉ có thể làm những việc mà ứng dụng có thể làm. Vẫn là điều không tốt, nhưng không tệ như có thể.
Bạn có thể giảm thiểu rủi ro hơn nữa bằng cách chia ứng dụng thành các phần logic và tạo hộp cát cho từng phần với đặc quyền tối thiểu có thể. Kỹ thuật này rất phổ biến trong mã gốc: ví dụ: Chrome tự chia thành một quy trình trình duyệt có đặc quyền cao có quyền truy cập vào ổ đĩa cứng cục bộ và có thể tạo kết nối mạng, cũng như nhiều quy trình trình kết xuất có đặc quyền thấp thực hiện việc phân tích cú pháp nội dung không đáng tin cậy. Trình kết xuất không cần chạm vào ổ đĩa, trình duyệt sẽ cung cấp cho trình kết xuất tất cả thông tin cần thiết để kết xuất trang. Ngay cả khi một hacker thông minh tìm ra cách làm hỏng trình kết xuất, thì họ cũng chưa đi được xa, vì trình kết xuất không thể tự làm được nhiều việc thú vị: tất cả quyền truy cập đặc quyền cao phải được định tuyến thông qua quy trình của trình duyệt. Kẻ tấn công sẽ cần tìm một số lỗ hổng trong các phần khác nhau của hệ thống để gây ra thiệt hại. Điều này làm giảm đáng kể nguy cơ xâm nhập thành công.
Cách an toàn để đưa eval()
vào hộp cát
Với tính năng hộp cát và API postMessage
, bạn có thể áp dụng mô hình này cho web một cách dễ dàng. Các phần của ứng dụng có thể nằm trong iframe
trong hộp cát và tài liệu mẹ có thể làm trung gian giao tiếp giữa các phần đó bằng cách đăng thông báo và nghe phản hồi. Loại cấu trúc này đảm bảo rằng các hành vi khai thác trong bất kỳ phần nào của ứng dụng đều gây ra thiệt hại tối thiểu có thể. Phương thức này cũng có ưu điểm là buộc bạn phải tạo các điểm tích hợp rõ ràng, nhờ đó, bạn biết chính xác những điểm cần thận trọng để xác thực dữ liệu đầu vào và đầu ra. Hãy cùng xem một ví dụ về đồ chơi để biết cách hoạt động của nó.
Evalbox là một ứng dụng thú vị lấy một chuỗi và đánh giá chuỗi đó dưới dạng JavaScript. Thật tuyệt phải không? Đúng là điều bạn đã chờ đợi suốt những năm qua. Tất nhiên, đây là một ứng dụng khá nguy hiểm vì việc cho phép JavaScript tuỳ ý thực thi có nghĩa là mọi dữ liệu mà một nguồn gốc cung cấp đều có thể bị lấy cắp. Chúng ta sẽ giảm thiểu rủi ro xảy ra Bad Things™ bằng cách đảm bảo rằng mã được thực thi bên trong hộp cát, giúp mã an toàn hơn rất nhiều. Chúng ta sẽ tìm hiểu mã từ trong ra ngoài, bắt đầu bằng nội dung của khung:
<!-- frame.html -->
<!DOCTYPE html>
<html>
<head>
<title>Evalbox's Frame</title>
<script>
window.addEventListener('message', function (e) {
var mainWindow = e.source;
var result = '';
try {
result = eval(e.data);
} catch (e) {
result = 'eval() threw an exception.';
}
mainWindow.postMessage(result, event.origin);
});
</script>
</head>
</html>
Bên trong khung, chúng ta có một tài liệu tối thiểu chỉ nghe các thông báo từ thành phần mẹ bằng cách nối vào sự kiện message
của đối tượng window
.
Bất cứ khi nào phần tử mẹ thực thi postMessage trên nội dung của iframe, sự kiện này sẽ kích hoạt, cho phép chúng ta truy cập vào chuỗi mà phần tử mẹ muốn chúng ta thực thi.
Trong trình xử lý, chúng ta lấy thuộc tính source
của sự kiện, đó là cửa sổ mẹ. Chúng ta sẽ sử dụng email này để gửi kết quả của công việc khó khăn này sau khi hoàn tất. Sau đó, chúng ta sẽ thực hiện công việc nặng bằng cách truyền dữ liệu đã được cung cấp vào eval()
. Lệnh gọi này đã được gói trong một khối try, vì các thao tác bị cấm bên trong iframe
trong hộp cát thường sẽ tạo ra các ngoại lệ DOM; chúng ta sẽ phát hiện các ngoại lệ đó và báo cáo một thông báo lỗi dễ hiểu. Cuối cùng, chúng ta đăng kết quả trở lại cửa sổ mẹ. Đây là một việc khá đơn giản.
Thành phần mẹ cũng tương tự như vậy. Chúng ta sẽ tạo một giao diện người dùng nhỏ với textarea
cho mã và button
để thực thi. Chúng ta sẽ kéo frame.html
thông qua iframe
trong hộp cát, chỉ cho phép thực thi tập lệnh:
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
id='sandboxed'
src='frame.html'></iframe>
Bây giờ, chúng ta sẽ kết nối mọi thứ để thực thi. Trước tiên, chúng ta sẽ nghe phản hồi từ iframe
và alert()
cho người dùng. Có thể một ứng dụng thực tế sẽ làm điều gì đó ít gây phiền toái hơn:
window.addEventListener('message',
function (e) {
// Sandboxed iframes which lack the 'allow-same-origin'
// header have "null" rather than a valid origin. This means you still
// have to be careful about accepting data via the messaging API you
// create. Check that source, and validate those inputs!
var frame = document.getElementById('sandboxed');
if (e.origin === "null" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
Tiếp theo, chúng ta sẽ kết nối trình xử lý sự kiện với các lượt nhấp vào button
. Khi người dùng nhấp chuột, chúng ta sẽ lấy nội dung hiện tại của textarea
và truyền nội dung đó vào khung để thực thi:
function evaluate() {
var frame = document.getElementById('sandboxed');
var code = document.getElementById('code').value;
// Note that we're sending the message to "*", rather than some specific
// origin. Sandboxed iframes which lack the 'allow-same-origin' header
// don't have an origin which you can target: you'll have to send to any
// origin, which might alow some esoteric attacks. Validate your output!
frame.contentWindow.postMessage(code, '*');
}
document.getElementById('safe').addEventListener('click', evaluate);
Có dễ không? Chúng tôi đã tạo một API đánh giá rất đơn giản và có thể chắc chắn rằng mã được đánh giá không có quyền truy cập vào thông tin nhạy cảm như cookie hoặc bộ nhớ DOM. Tương tự, mã được đánh giá không thể tải trình bổ trợ, bật lên cửa sổ mới hoặc bất kỳ hoạt động phiền toái hoặc độc hại nào khác.
Bạn có thể làm tương tự cho mã của riêng mình bằng cách chia các ứng dụng nguyên khối thành các thành phần có mục đích duy nhất. Mỗi thông báo có thể được gói trong một API nhắn tin đơn giản, giống như những gì chúng ta đã viết ở trên. Cửa sổ mẹ có đặc quyền cao có thể đóng vai trò là trình điều khiển và trình điều phối, gửi thông báo đến các mô-đun cụ thể, mỗi mô-đun có ít đặc quyền nhất có thể để thực hiện công việc, theo dõi kết quả và đảm bảo rằng mỗi mô-đun chỉ được cung cấp thông tin cần thiết.
Tuy nhiên, bạn cần phải hết sức cẩn thận khi xử lý nội dung được đóng khung có cùng nguồn gốc với nội dung mẹ. Nếu một trang trên https://example.com/
đóng khung một trang khác trên cùng một nguồn gốc bằng một hộp cát bao gồm cả cờ allow-same-origin và allow-scripts, thì trang được đóng khung có thể truy cập vào trang mẹ và xoá hoàn toàn thuộc tính hộp cát.
Chơi trong hộp cát
Hiện tại, bạn có thể sử dụng tính năng hộp cát trên nhiều trình duyệt: Firefox 17 trở lên, IE10 trở lên và Chrome tại thời điểm viết bài (tất nhiên, caniuse có bảng hỗ trợ mới nhất). Việc áp dụng thuộc tính sandbox
cho iframes
mà bạn đưa vào cho phép bạn cấp một số đặc quyền nhất định cho nội dung mà chúng hiển thị, chỉ những đặc quyền cần thiết để nội dung hoạt động đúng cách. Điều này giúp bạn có cơ hội giảm thiểu rủi ro liên quan đến việc đưa nội dung của bên thứ ba vào, ngoài những gì có thể thực hiện được với Chính sách bảo mật nội dung.
Hơn nữa, hộp cát là một kỹ thuật hiệu quả để giảm nguy cơ một kẻ tấn công thông minh có thể khai thác lỗ hổng trong mã của chính bạn. Bằng cách tách một ứng dụng nguyên khối thành một tập hợp các dịch vụ trong hộp cát, mỗi dịch vụ chịu trách nhiệm về một phần nhỏ chức năng độc lập, kẻ tấn công sẽ buộc phải không chỉ xâm phạm nội dung của một số khung cụ thể mà còn xâm phạm cả bộ điều khiển của các khung đó. Đó là một nhiệm vụ khó khăn hơn nhiều, đặc biệt là vì phạm vi của bộ điều khiển có thể giảm đáng kể. Bạn có thể dành thời gian kiểm tra mã đó liên quan đến bảo mật nếu yêu cầu trình duyệt trợ giúp phần còn lại.
Điều đó không có nghĩa là hộp cát là giải pháp hoàn chỉnh cho vấn đề bảo mật trên Internet. Phương pháp này cung cấp khả năng phòng thủ chuyên sâu và trừ phi bạn có quyền kiểm soát ứng dụng của người dùng, bạn chưa thể dựa vào tính năng hỗ trợ trình duyệt cho tất cả người dùng (nếu bạn kiểm soát ứng dụng của người dùng – ví dụ: môi trường doanh nghiệp – thì thật tuyệt!). Một ngày nào đó… nhưng hiện tại, hộp cát là một lớp bảo vệ khác để tăng cường khả năng phòng thủ của bạn, chứ không phải là một lớp bảo vệ hoàn chỉnh mà bạn có thể dựa vào. Tuy nhiên, các lớp vẫn rất tuyệt vời. Bạn nên sử dụng phương thức này.
Tài liệu đọc thêm
"Phân tách đặc quyền trong ứng dụng HTML5" là một bài viết thú vị, trình bày cách thiết kế một khung nhỏ và ứng dụng khung đó cho ba ứng dụng HTML5 hiện có.
Cơ chế hộp cát có thể linh hoạt hơn nữa khi kết hợp với hai thuộc tính iframe mới khác:
srcdoc
vàseamless
. Phương thức trước cho phép bạn điền nội dung vào khung mà không tốn chi phí của yêu cầu HTTP, còn phương thức sau cho phép kiểu được truyền vào nội dung được đóng khung. Cả hai đều có khả năng hỗ trợ trình duyệt khá tệ tại thời điểm này (Chrome và bản phát hành hằng đêm của WebKit), nhưng sẽ là một sự kết hợp thú vị trong tương lai. Ví dụ: bạn có thể đưa ra nhận xét trong hộp cát về một bài viết thông qua mã sau:<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>