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ư chắc chắn liên quan đến việc nhúng các thành phần và nội dung mà bạn không có quyền kiểm soát thực sự. 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, thậm chí 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. Bạn không thể tránh cả hai, nhưng cả hai đều làm tăng nguy cơ xảy ra điều gì đó không hay 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ăng cho những người có ý đồ 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 đúng hướng, nhưng cần lưu ý rằng 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. Đôi khi, bạn nên nói rằng "Tôi không chắc là tôi thực sự tin tưởng nguồn nội dung này, nhưng nó rất hay! Trình duyệt ơi, hãy nhúng nó vào, nhưng đừng để nó làm hỏng trang web của tôi nhé."

Đặc quyền tối thiểu

Về cơ bản, 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 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 bật lên cửa sổ mới, thì việc loại bỏ quyền truy cập vào window.open sẽ không gây hại. Nếu trang web không yêu cầu Flash, thì việc tắt tính năng hỗ trợ trình bổ trợ sẽ không gây ra vấn đề gì. Chúng ta sẽ được bảo mật tối đa nếu tuân theo 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 nên sử dụng. Ứng dụng sẽ không có quyền truy cập vào chức năng đó ngay từ đầu.

iframe phần tử 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 tách biệt ứng dụng của bạn 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 bạn đã lưu trữ cục bộ, cũng như không thể vẽ đến 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 tách biệt này không thực sự hiệu quả. Trang chứa vẫn có một số lựa chọn cho hành vi gây phiền nhiễu hoặc độc hại: video tự động phát, trình bổ trợ và cửa sổ bật lên là phần nổi của tảng băng chìm.

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

Tin tưởng nhưng đừng quên xác minh

Nút "Tweet" của Twitter là một ví dụ điển hình 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 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 xem chúng ta có thể khoá những gì, hãy xem xét kỹ những chức năng mà nút này yêu cầu. HTML được tải vào khung sẽ thực thi một số JavaScript từ máy chủ của Twitter và tạo một cửa sổ bật lên có giao diện đăng bài trên Twitter khi người dùng 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. Chỉ có vậy thôi; 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 đến các đặc quyền đó, hãy xoá chúng bằng cách đưa nội dung của khung vào hộp cát.

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ả quyền có thể, sau đó bật lại từng chức năng 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ể làm như vậ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 cần thiết và trình duyệt sẽ từ chối 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 cho khung 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 vài cờ hộp cát có thể có trong ví dụ trên, giờ hãy tìm hiểu kỹ hơn về hoạt động bên trong của thuộc tính này.

Nếu một iframe có thuộc tính hộp cát trống, thì tài liệu trong khung sẽ được đưa vào hộp cát hoàn toàn, khiến tài liệu đó phải tuân theo các quy định 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 cả trình xử lý sự kiện cùng dòng và URL javascript:. Điều này cũng có nghĩa là nội dung có trong thẻ noscript sẽ được hiển thị, giống hệt như khi người dùng đã tự tắt tập lệnh.
  • Tài liệu được đóng khung sẽ được tải vào một nguồn gốc duy nhất, 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 bao giờ khớp với bất kỳ nguồn gốc nào khác, kể cả 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, DB đượ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 tài liệu 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 có target="_top" sẽ không có hiệu lực.
  • Các tính năng tự động kích hoạt (phần tử biểu mẫu tự động lấy tiêu điểm, video tự động phát, v.v.) sẽ bị chặn.
  • Không thể 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à một tài liệu được tải vào một iframe hoàn toàn trong hộp cát thực sự ít gây rủi ro. Tất nhiên, iframe này cũng không thể làm được gì nhiều: bạn có thể thoát khỏi hộp cát đầ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ọi thứ một chút.

Ngoại trừ trình bổ trợ, bạn có thể dỡ bỏ từng quy định hạn chế này bằng cách thêm một 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ờ có thể chạy 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 (bất ngờ chưa!).
  • allow-pointer-lock cho phép khoá con trỏ (bất ngờ chưa!).
  • 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, đồng thời cho phép các tính năng tự động kích hoạt (vì chúng sẽ không đáng kể để triển khai qua JavaScript).
  • 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ó được tập hợp cờ hộp cát cụ thể trong ví dụ về Twitter ở trên:

  • allow-scripts là bắt buộc, 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.
  • allow-popups là bắt buộc, vì nút này sẽ bật lên một biểu mẫu đăng bài trên Twitter trong một cửa sổ mới.
  • allow-forms là bắt buộc, vì biểu mẫu đăng bài trên Twitter phải 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ác 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, ngay cả khi biểu mẫu chỉ tồn tại trong cửa sổ mà khung bật lên.

Khi có thuộc tính sandbox, tiện ích chỉ nhận được các quyền mà tiện ích đó yêu cầu, còn các chức năng như trình bổ trợ, thao tác điều hướng 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 gây ra tác động xấu. Đây là một kết quả có lợi cho tất cả các bên liên quan.

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

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

Tôi muốn đặt câu hỏi ngược lạ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ợ? Trong trường hợp tốt nhất, đó là một đặc quyền mà bạn không bao giờ sử dụng, còn trong trường hợp xấu nhất, đó 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à trên thực tế, mọi ứng dụng đều có thể bị khai thác theo cách này hay cách khác. Việc đưa mã của riêng bạn vào hộp cát có nghĩa là ngay cả khi kẻ tấn công lật đổ thành công ứ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 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ư 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à đưa từng phần vào hộp cát 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 kết xuất có đặc quyền thấp thực hiện công việc nặng nhọc là 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ẽ đảm nhận việc cung cấp cho trình kết xuất tất cả thông tin cần thiết để kết xuất một trang. Ngay cả khi một tin tặc thông minh tìm ra cách làm hỏng trình kết xuất, thì tin tặc đó cũng không thể tiến xa vì trình kết xuất không thể tự làm được gì nhiều: tất 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ẽ 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ơ bị tấn công thành công.

Chạy eval() trong hộp cát một cách an toàn

Với hộp cát và postMessage API, mô hình này có thể dễ dàng áp dụng thành công cho web. Các phần của ứng dụng có thể nằm trong iframe hộp cát và tài liệu gốc 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à lắng nghe phản hồ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ách này cũng có lợi thế 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 nơi cần cẩn thận khi xác thực dữ liệu đầu vào và đầu ra. Hãy cùng xem qua một ví dụ đơn giản để biết cách hoạt động của tính năng này.

Evalbox là một ứng dụng thú vị, nhận một chuỗi và đánh giá chuỗi đó dưới dạng JavaScript. Thật ấn tượng, đúng không? Chính 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 thực thi JavaScript tuỳ ý 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 mất. Chúng ta sẽ giảm thiểu rủi ro xảy ra những điều không mong muốn 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ã an toàn hơn một chút. Chúng ta sẽ xử lý 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ỉ đơn giản là lắng nghe 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, tức là cửa sổ mẹ. Chúng tôi sẽ sử dụng thông tin này để gửi kết quả của công việc khó khăn của chúng tôi sau khi chúng tôi 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 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ẽ nắm bắt những lỗi đó và báo cáo 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 đơn giản tương tự. Chúng ta sẽ tạo một giao diện người dùng nhỏ có textarea cho mã và button để thực thi, đồng thời chúng ta sẽ lấy 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ẽ lắng nghe phản hồi từ iframe và alert() cho người dùng. Có lẽ một ứng dụng thực tế sẽ làm điều gì đó ít gây phiền nhiễ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 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 vào, chúng ta sẽ lấy nội dung hiện tại của textarea và chuyể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 cửa sổ mới hoặc thực hiện bất kỳ hoạt động nào khác gây phiền toái hoặc độc hại.

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ành phần 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à bộ đ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à 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, xin lưu ý rằng 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ể tiếp cận 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

Hộp cát hiện có sẵn cho bạn trong nhiều trình duyệt: Firefox 17+, IE10+ 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à các phần tử đó hiển thị, chỉ những đặc quyền cần thiết để nội dung hoạt động chính xác. Điều này giúp bạn 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ơ kẻ tấn công tinh vi có thể khai thác lỗ hổng trong mã của 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ụ 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 xâm phạm không chỉ nội dung của các khung cụ thể mà còn cả 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à vì bộ điều khiển có thể bị giảm đáng kể về 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 bạn 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à một giải pháp hoàn chỉnh cho vấn đề bảo mật trên Internet. Tính năng này cung cấp khả năng phòng thủ theo chiều sâu và trừ phi bạn có quyền kiểm soát đối với ứ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, chẳng hạn như môi trường doanh nghiệp, thì xin chúc mừng!). Có thể 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ủ, chứ không phải là một biện pháp phòng thủ hoàn chỉnh mà bạn có thể hoàn toàn dựa vào. Tuy nhiên, các lớp vẫn rất hữu ích. Tôi khuyên bạn nên sử dụng mẫu 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ị về 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ó.

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