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 ngày nay gần như không thể tránh khỏi việc bao gồm các thành phần và nội dung được nhúng mà bạn không có quyền kiểm soát thực sự. Cá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 người dùng tổng thể. Ngoài ra, nội dung do người dùng tạo đôi khi còn quan trọng hơn nội dung gốc của trang web. Việc tránh sử dụng một trong hai nội dung trên thực sự không phải là một lựa chọn, nhưng cả hai sẽ làm tăng nguy cơ Xảy ra lỗiTM có thể xảy ra trên trang web của bạn. Mỗi tiện ích mà bạn nhúng vào – 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ăng cho những tiện ích có ý định xấu:

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 quan trọng theo hướng phù hợp, 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à tệp nhị phân: tài nguyên được cho phép hoặc không được phép. Đôi khi sẽ rất hữu ích nếu bạn nói "Tôi không chắc chắn là mình thực sự tin tưởng nguồn nội dung này, nhưng nó thật sự hay! Vui lòng nhúng Trình duyệt, nhưng đừng để trình duyệt này phá vỡ trang web của tôi".

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

Về cơ bản, chúng tôi đang tìm một cơ chế cho phép chúng tôi cấp nội dung mà chúng tôi chỉ nhúng được ở mức tối thiểu cần thiết để thực hiện công việc đó. Nếu một tiện ích không cần bật cửa sổ mới, thì việc xoá quyền truy cập vào window.open không thể gây tổn hại. Nếu trình bổ trợ không yêu cầu Flash, thì bạn không nên tắt tính năng hỗ trợ trình bổ trợ. Chúng tôi an toàn ở mức có thể nếu chúng tôi tuân theo nguyên tắc về đặc quyền tối thiểu và chặn từng tính năng không liên quan trực tiếp đến chức năng mà chúng tôi muốn sử dụng. Kết quả là chúng ta không còn phải mơ hồ tin tưởng rằng một số phần nội dung nhúng sẽ không tận dụng được những đặc quyền mà lẽ ra chúng không nên sử dụng. Chỉ đơ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 để xây dựng một khung hiệu quả cho một 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ẽ là thước đo sự phân tách giữa ứng dụng và nội dung bạn muốn tải. Nội dung được tạo 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 của đường viền của khung. Tuy nhiên, sự 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 khó chịu hoặc độc hại: tự động phát video, trình bổ trợ và cửa sổ bật lên.

Thuộc tính sandbox của phần tử iframe cung cấp cho chúng ta những gì cần thiết để thắt chặt các quy định hạn chế đối với nội dung hiển thị trong 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 quyền thấp, chỉ cho phép một số tính năng cần thiết để thực hiện bất kỳ nhu cầu công việc nào.

Twust, nhưng hãy xác minh

Nút "Tweet" trên Twitter là một ví dụ hay về chức năng có thể được nhúng một cách an toàn hơn trên trang web thông qua hộp cát. Twitter cho phép bạn nhúng nút qua iframe với 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ỹ xem nút này yêu cầu những chức năng nào. HTML được tải vào khung sẽ thực thi một số JavaScript từ các 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 xoay khi người dùng nhấp vào. Giao diện đó cần quyền truy cập vào cookie của Twitter để liên kết tweet với đúng tài khoản, đồng thời cần có khả năng gửi biểu mẫu tweet. Thế là xong; khung hình không cần tải bất kỳ trình bổ trợ nào, cũng 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ì khung hình không cần các đặc quyền đó, nên bạn hãy xoá chúng 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 tôi bắt đầu bằng cách xoá tất cả quyền có thể, sau đó bật lại từng chức năng 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, 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 tất cả các khả năng cần thiết và trình duyệt sẽ từ chối cấp cho khung quyền truy cập bất kỳ đặc quyền nào mà chúng tôi 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 tìm hiểu hoạt động bên trong của thuộc tính một cách chi tiết hơn một chút.

Với một iframe có thuộc tính hộp cát trống, tài liệu được tạo khung sẽ được tạo khung 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 tạo 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ị, giống như thể người dùng đã tự tắt tập lệnh.
  • Tài liệu có khung được tải vào một nguồn gốc riêng biệt, có nghĩa là tất cả các bước 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 khớp với bất cứ nguồn gốc nào khác, thậm chí chính chúng. Ngoài 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 bất kỳ cookie của 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 tạo 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 tạo khung chỉ có thể tự điều hướng, chứ không phải 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ằng target="_top" sẽ không có hiệu lực.
  • Các tính năng kích hoạt tự động (thành phần hình dạng được tự động lấy nét, tự động phát video, v.v.) sẽ bị chặn.
  • Không thể lấy được khoá con trỏ.
  • Thuộc tính seamless bị bỏ qua trên iframes có trong tài liệu được tạo khung.

Điều này thực sự quá đặc biệt và tài liệu được tải vào một iframe hoàn toàn trong hộp cát thực sự có rất ít rủi ro. Tất nhiên, việc này cũng không mang lại nhiều giá trị: bạn có thể thoát khỏi hộp cát đầy đủ cho một số nội dung tĩnh, nhưng trong hầu hết trường hợp, bạn sẽ muốn nới lỏng mọi thứ một chút.

Ngoại trừ các trình bổ trợ, bạn có thể gỡ bỏ từng 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. Các 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, còn mọi thứ khác thì vẫn ổn:

  • allow-forms cho phép gửi biểu mẫu.
  • allow-popups cho phép hiển thị cửa sổ bật lên (gây sốc!)
  • allow-pointer-lock cho phép khoá con trỏ (ngạc nhiên!)
  • allow-same-origin cho phép tài liệu giữ nguyên nguồn gốc; các trang được tải từ https://example.com/ sẽ giữ nguyên 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, đồng thời cho phép các tính năng tự động kích hoạt (vì chúng không đơn giản khi triển khai thông qua JavaScript).
  • 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 chúng tôi chọn tập hợp cờ hộp cát cụ thể trong ví dụ trên Twitter ở trên:

  • allow-scripts là bắt buộc, vì trang được tải vào khung chạy một số JavaScript để xử lý tương tác của người dùng.
  • allow-popups là bắt buộc vì nút này bật lên biểu mẫu tweet trong một cửa sổ mới.
  • allow-forms là bắt buộc vì biểu mẫu tweet phải có thể gửi được.
  • allow-same-origin là cần thiết vì nếu không, cookie của twitter.com sẽ không truy cập được và người dùng không thể đăng nhập để đăng biểu mẫu.

Một điều quan trọng cần lưu ý là cờ hộp cát được á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. Tức 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 có sẵn thuộc tính sandbox, tiện ích chỉ nhận được các quyền cần thiết, đồng thời các chức năng như trình bổ trợ, thao tác ở phía trên cùng và khoá con trỏ vẫn bị chặn. Chúng tôi đã giảm rủi ro khi nhúng tiện ích mà không để lại hiệu ứng xấu. Việc này mang lại lợi ích cho tất cả những người có liên quan.

Phân tách đặc quyền

Việc hộp cát nội dung của bên thứ ba để chạy mã không đáng tin cậy của họ trong một môi trường đặc quyền thấp rõ ràng là mang lại lợi ích. Nhưng còn mã của bạn thì sao? Bạn tin tưởng bản thân mình, đúng không? Vậy tại sao phải lo lắng về hộp cát?

Tôi sẽ xoay lại 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à một đặc quyền mà bạn không bao giờ sử dụng, tệ nhất là đó là một vector tiềm năng để những kẻ tấn công xâm nhập vào hệ thống. Mã của mỗi người đều có lỗi và gần như mọi ứng dụng đều dễ bị khai thác theo cách này hay cách khác. Việc đóng hộp cát cho mã của riêng bạn có nghĩa là ngay cả khi một kẻ tấn công đảo ngược thành công ứng dụng của bạn, thì chúng cũ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ể làm những việc mà ứng dụng có thể làm. Vẫn tệ nhưng không tệ như mong đợi.

Bạn có thể giảm rủi ro hơn nữa bằng cách chia nhỏ ứng dụng thành các phần logic và tạo hộp cát cho mỗi 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ự 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 các kết nối mạng, còn nhiều quy trình kết xuất đồ hoạ 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 phải chạm vào ổ đĩa, trình duyệt sẽ đảm nhận việc cung cấp tất cả thông tin cần thiết để kết xuất trang. Ngay cả khi một tin tặc thông minh tìm được cách làm hỏng trình kết xuất, cô vẫn chưa làm được gì nhiều vì bản thân trình kết xuất đó không thực sự quan tâm: tất cả các quyền truy cập đặc quyền cao đều phải được định tuyến thông qua quy trình của trình duyệt. Kẻ tấn công sẽ phải tìm một số lỗ trên các phần khác nhau của hệ thống để gây bất kỳ thiệt hại nào, giúp giảm đáng kể nguy cơ phảm hoạ thành công.

Hộp cát an toàn eval()

Với hộp cát và API postMessage, sự thành công của mô hình này khá đơn giản để áp dụng cho web. Các phần của ứng dụng có thể nằm trong các iframe hộp cát và tài liệu mẹ có thể thực hiện hoạt động giao tiếp giữa các phần tử đó bằng cách đăng thông báo và lắng nghe phản hồi. Kiểu cấu trúc này đảm bảo rằng việc khai thác trong bất kỳ phần nào của ứng dụng sẽ gây ra thiệt hại tối thiểu có thể. Phương thức này cũng có lợi thế là buộc bạn tạo các điểm tích hợp rõ ràng, nhờ đó, bạn biết chính xác nơi cần cẩn thận trong việc 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 để xem cách này có thể hoạt động như thế nào.

Evalbox là một ứng dụng thú vị. Ứng dụng này nhận một chuỗi và đánh giá dưới dạng JavaScript. Chà, đúng không? Điều bạn mong chờ bấy lâu nay là gì. Tất nhiên, đây là một ứng dụng khá nguy hiểm vì việc cho phép thực thi JavaScript tuỳ ý có nghĩa là bất kỳ và mọi dữ liệu mà một nguồn gốc cung cấp đều có thể được lấy. Chúng tôi sẽ giảm thiểu nguy cơ Bad ThingsTM xảy ra bằng cách đảm bảo rằng mã được thực thi bên trong một hộp cát, giúp mã trở nên an toàn hơn một chút. Chúng ta sẽ xử lý mã từ trong ra ngoài, bắt đầu với 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 này, chúng ta có một tài liệu tối thiểu chỉ để nghe các thông báo của 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 thành phần mẹ thực thi postMessage trên nội dung của iframe, sự kiện này sẽ kích hoạt, cấp cho chúng ta quyền truy cập vào chuỗi mà thành phần mẹ muốn chúng tôi thực thi.

Trong trình xử lý, chúng ta lấy thuộc tính source của sự kiện, đây là cửa sổ mẹ. Chúng tôi sẽ sử dụng thông báo này để gửi kết quả nỗ lực của mình trở lại sau khi hoàn tất. Sau đó, chúng ta sẽ thực hiện phần việc khó khăn 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 gọn trong một khối dùng thử, vì các thao tác bị cấm bên trong iframe hộp cát sẽ thường tạo ra các ngoại lệ DOM; chúng tôi sẽ phát hiện các thao tác đó 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à 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 để thực thi mã và một button để thực thi. Chúng ta sẽ kéo frame.html thông qua một iframe dạng 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ẽ lắng nghe phản hồi của iframealert() của người dùng. Có lẽ một ứng dụng thực sẽ làm điều gì đó ít khó chịu 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 một trình xử lý sự kiện để nhận các lượt nhấp vào button. Khi người dùng nhấp vào, chúng ta sẽ lấy nội dung hiện tại của textarea và chuyển các 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);

Thật dễ dàng đúng không? Chúng tôi đã tạo một API đánh giá rất đơn giản và có thể đảm bảo 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ự như vậy, mã đã đánh giá không thể tải các trình bổ trợ, cửa sổ mới bật lên hoặc bất kỳ hoạt động độc hại hoặc gây khó chịu 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 đơn dụng. Mỗi tham số có thể được gói gọn trong một API nhắn tin đơn giản, giống như những gì chúng tôi đã viết ở trên. Cửa sổ mẹ có đặc quyền cao có thể hoạt động như bộ điều khiển và trình điều phối, gửi thông báo tới các mô-đun cụ thể mà mỗi mô-đun có ít đặc quyền nhất có thể để thực hiện công việc, lắng nghe kết quả và đảm bảo rằng mỗi mô-đun đều được cung cấp tốt chỉ với thông tin cần thiết.

Tuy nhiên, hãy lưu ý rằng bạn cần phải hết sức cẩn thận khi xử lý nội dung đóng khung có cùng nguồn gốc với thành phần mẹ. Nếu một trang trên https://example.com/ tạo khung hình cho một trang khác trên cùng một nguồn gốc có hộp cát bao gồm cả cờ allow-same-originallow-scripts, thì trang được tạo khung có thể truy cập vào phần tử mẹ và xoá hoàn toàn thuộc tính hộp cát.

Chơi trong hộp cát

Hộp cát hiện có sẵn cho bạn 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 (tất nhiên, có thể 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 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 rủi ro liên quan đến việc đưa nội dung của bên thứ ba vào, cao hơn và cao hơn những gì đã có trong 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 mạnh mẽ giúp giảm nguy cơ mà những kẻ tấn công thông minh có thể khai thác các lỗ hổng trong mã của bạn. Bằng cách phân tách một ứng dụng nguyên khối thành một nhóm dịch vụ hộp cát, mỗi dịch vụ chịu trách nhiệm cho một phần nhỏ chức năng độc lập, những kẻ tấn công sẽ buộc phải không chỉ xâm phạm nội dung của các khung cụ thể mà còn xâm phạm bộ điều khiển của chúng. Đó là một nhiệm vụ khó khăn hơn nhiều, đặc biệt là khi bộ điều khiển có thể bị giảm đáng kể trong phạm vi. Bạn có thể dành nỗ lực liên quan đến bảo mật để kiểm tra mã đó nếu yêu cầu trình duyệt trợ giúp các bước còn lại.

Điều đó không có nghĩa là hộp cát là một giải pháp hoàn chỉnh cho vấn đề bảo mật trên Internet. Hộp cát về quyền riêng tư cung cấp khả năng bảo vệ chuyên sâu và trừ khi bạn có quyền kiểm soát ứng dụng của người dùng, bạn sẽ không thể dựa vào sự hỗ trợ của trình duyệt cho tất cả người dùng (nếu bạn kiểm soát được máy khách của người dùng – môi trường doanh nghiệp chẳng hạn – tuyệt vời!). 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 hàng thủ của bạn, nó không phải là 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 ở đây rất tuyệt vời. Tôi khuyên bạn nên tận dụng mụ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ị, hoạt động xuyên suốt thiết kế của một khung nhỏ và ứng dụng của khung đó cho ba ứng dụng HTML5 hiện có.

  • Hộp cát thậm chí còn linh hoạt hơn khi kết hợp với hai thuộc tính iframe mới khác: srcdocseamless. Phương thức thứ hai cho phép bạn điền sẵn nội dung vào khung mà không cần hao tổn yêu cầu HTTP, còn cách thứ hai cho phép chuyển kiểu vào nội dung trong khung. Cả hai đều hỗ trợ trình duyệt khá tệ ở thời điểm hiện tại (các đêm Chrome và WebKit). Tuy nhiên, chúng sẽ là một sự kết hợp thú vị trong tương lai. Ví dụ: bạn có thể tạo hộp cát cho nhận xét trên 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>