Chơi một cách an toàn trong các IFrames hộp cát

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 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 khác đáng tin cậy vào danh sách cho phép. Đâ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 chỉ thị 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."

Đặc quyền thấp nhất

Về bản chất, chúng tôi đang tìm kiếm một cơ chế cho phép chúng tôi cấp cho nội dung mà chúng tôi nhúng chỉ ở mức khả năng tối thiểu cần thiết để thực hiện công việc của mình. Nếu một tiện ích không cần phải bật cửa sổ mới lên, thì bạn không thể lấy quyền truy cập vào window.open. 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. Đơn giản là ban đầu ứng dụng đó sẽ không có quyền truy cập vào chức năng đó.

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 đặt trong 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 đối với đườ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" của Twitter là một ví dụ hay về chức năng có thể được nhúng 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 ra những chức năng 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. Vậy là xong; 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ỳ bit 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.

Tính năng hộp cát hoạt động dựa trên danh sách cho phép. Chúng tôi bắt đầu bằng cách xoá mọi quyền có thể có, sau đó bật lại các tính năng riêng lẻ bằng cách thêm 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 ta đã cung cấp cho khung này tất cả tính năng cần thiết và trình duyệt sẽ từ chối một cách hữu ích quyền truy cập của khung đó vào bất kỳ đặc quyền nào mà chúng ta không cấp một cách 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 trình xử lý sự kiện cùng dòng 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ị, chính xác 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ặc target="_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 thể điều hướng ở 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ằng target="_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 tìm được khoá con trỏ.
  • Thuộc tính seamless bị bỏ qua trên iframes 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 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 kinh ngạc!).
  • 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 ra khỏi khung bằng cách di chuyển đến 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.
  • Cần có allow-forms vì biểu mẫu tweet phải là biểu mẫu có thể gửi.
  • 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 áp dụng thuộc tính sandbox, tiện ích này chỉ nhận được các quyền cần thiết, còn các tính năng như trình bổ trợ, thanh đ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 bất kỳ tác động xấu nào. Đâ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ẽ xoay quanh câu hỏi đó: nếu mã của bạn không cần trình bổ trợ, tại sao lại cấp cho mã 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 hộp cát mã của riêng bạn có nghĩa là ngay cả khi kẻ tấn công phá vỡ ứng dụng thành công, chúng sẽ không được cấp toàn bộ quyền truy cập vào nguồn gốc của ứng dụng; chúng sẽ chỉ có thể thực hiện những việc mà ứng dụng có thể làm. Vẫn tệ, nhưng không tệ bằng.

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: Chẳng hạn, Chrome tự phá vỡ 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 kết xuất trình kết xuất có đặc quyền thấp giúp thực hiện phần lớ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 trên các phần khác nhau của hệ thống để gây thiệt hại, từ đó giúp giảm đáng kể nguy cơ cướp đồ.

Tạo hộp cát an toàn cho eval()

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ị, có thể nhận 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 một chút. 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 giản chỉ theo dõi thông báo từ phần tử mẹ bằng cách kết nối với 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 thử, vì các thao tác bị cấm bên trong iframe hộp cát sẽ thường xuyên tạo ra các ngoại lệ DOM; chúng tôi sẽ phát hiện các ngoại lệ đó và báo cáo một thông báo lỗi thân thiện. Cuối cùng, chúng ta đăng kết quả trở lại cửa sổ mẹ. Đây là những nội dung khá đơn giản.

Thành phần mẹ cũng không phức tạp tương tự. Chúng ta sẽ tạo một giao diện người dùng nhỏ có textarea để mã và một button để thực thi, sau đó chúng ta sẽ kéo frame.html qua một 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ừ iframealert() 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" &amp;&amp; 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 vào các mô-đun cụ thể, trong đó mỗi mô-đun có ít đặc quyền nhất có thể để thực hiện công việc của mình, theo dõi kết quả và đảm bảo rằng mỗi mô-đun chỉ được cung cấp đầy đủ 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 gốc. 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-originallow-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 của bạn

Tính năng Hộp cát hiện có sẵn cho bạ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, ứng dụng này có bảng hỗ trợ cập nhật). Việc áp dụng thuộc tính sandbox cho iframes mà bạn đưa vào sẽ 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 có 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: srcdocseamless. Phương thức trước cho phép bạn điền nội dung vào khung mà không cầ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. Chẳng hạn, bạn có thể đưa ra nhận xét về hộp cát trong một bài viết qua mã sau:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>