Web Security for Frontend Devs · Part 23 — iframe Sandboxing & Third-Party Widget Isolation
Bonus track: every third-party widget you embed can submit forms, open popups, even navigate your whole tab. How the sandbox attribute flips that to deny-by-default, the allow-scripts + allow-same-origin footgun, and a safe pattern.
Phần 23 — Nhánh bonus trong series Web Security for Frontend Devs. Trước: Tiếp:
Bạn nhúng một widget bên thứ ba — khung chat, quảng cáo, comment, một analytics snippet, một bản preview rich-text. Một dòng <iframe> tưởng vô hại. Nhưng theo mặc định, iframe đó có nhiều quyền hơn bạn nghĩ rất nhiều: chạy script, gửi form, mở popup, và — cú đau nhất — điều hướng cả tab của bạn sang trang phishing.
Phần này về công cụ cô lập mạnh nhất mà nền tảng cho không: thuộc tính sandbox. Nó lật thế cờ — từ “cho phép mọi thứ” sang “cấm tất cả, rồi cấp lại từng quyền” — và về cái bẫy khiến nhiều người vô tình vô hiệu hoá chính nó.
Mô hình tư duy: deny-by-default
iframe thường kế thừa gần như mọi năng lực của một trang đầy đủ. Thêm sandbox (rỗng) đặt nó về mức hạn chế tối đa:
<iframe src="https://widget.thirdparty.example/embed" sandbox></iframe>
Với sandbox="", nội dung trong khung không chạy được script, không gửi form, không mở popup, bị ép vào origin opaque (duy nhất), và không chạm tới trang cha. Bạn cấp lại năng lực bằng các token — và đây là điểm hay: bạn phải chủ ý mỗi quyền.
<iframe src="…" sandbox="allow-scripts allow-popups"></iframe>
Tư duy:
sandboxlà một allowlist. Bắt đầu từ rỗng, thêm đúng cái widget thực sự cần — không hơn.
Các token thường gặp
| Token | Cấp quyền gì | Mức nguy hiểm |
|---|---|---|
allow-scripts | chạy JavaScript | cần cho hầu hết widget |
allow-same-origin | giữ nguyên origin (không opaque) | cẩn trọng — xem phần dưới |
allow-forms | gửi form | trung bình |
allow-popups | window.open | trung bình (popup kế thừa sandbox) |
allow-top-navigation | đổi top.location | gần như không bao giờ cấp |
allow-modals | alert/confirm/print | thấp (phiền/scareware) |
allow-downloads | khởi động tải file | trung bình |
Hai dòng tô đậm là nơi tai nạn xảy ra.
Cú footgun: allow-scripts + allow-same-origin
Đây là lỗi cấu hình sandbox phổ biến nhất. Nếu khung có nguồn same-origin (hoặc srcdoc/blob: kế thừa origin bạn) và bạn cấp cả allow-scripts lẫn allow-same-origin:
<!-- nội dung same-origin: sandbox tự huỷ -->
<iframe src="/widget.html" sandbox="allow-scripts allow-same-origin"></iframe>
Script trong khung giữ nguyên origin của bạn, nên nó với tới parent.document, tìm chính phần tử <iframe> của mình, xoá thuộc tính sandbox, rồi reload — giờ chạy không hề bị giới hạn. Sandbox trở thành vô nghĩa.
Spec ghi rõ cảnh báo này. Quy tắc: đừng bao giờ kết hợp allow-scripts và allow-same-origin cho nội dung same-origin mà bạn không hoàn toàn tin. Nếu cần script và cô lập, hãy phục vụ widget từ một origin khác (subdomain hoặc domain sandbox riêng), để allow-same-origin không trao lại quyền vào origin của bạn.
Lưu ý SOP: với widget cross-origin, Same-Origin Policy đã chặn truy cập trang cha bất kể
allow-same-origin. Nguy hiểm chỉ bùng lên khi origin của khung trùng với bạn.
Đừng cho widget điều hướng tab của bạn
allow-top-navigation cho phép khung gán top.location — tức vẽ lại toàn bộ tab. Một quảng cáo độc hoặc widget bị chiếm có thể đẩy người dùng sang trang lừa đảo:
// chạy trong khung nếu có allow-top-navigation:
top.location = "https://your-bank.example.evil/login";
Hầu như không bao giờ cấp nó. Nếu một luồng hợp pháp cần điều hướng (vd nút “thanh toán”), dùng allow-top-navigation-by-user-activation — chỉ cho phép khi có tương tác thật của người dùng, chặn redirect tự động. Đây là họ hàng của reverse tabnabbing (Phần 18): lạm dụng quan hệ điều hướng, không phải chèn code.
Thử ngay — trình mô phỏng sandbox
Chọn nguồn widget (cross-origin / same-origin / srcdoc) và bật/tắt từng token, rồi xem một widget độc hại làm được gì và bị chặn gì — gồm cả lúc sandbox tự huỷ vì allow-scripts + allow-same-origin. Hoàn toàn mô phỏng; không nhúng khung thật.
Mở demo đầy đủ:
Lab thực hành — tái hiện an toàn
Chứng minh deny-by-default và cú escape trong trình duyệt thật.
Chuẩn bị
mkdir -p /tmp/sbx && cd /tmp/sbx
# widget "độc": thử đọc cha, gửi form, đổi top, mở popup
cat > widget.html <<'EOF'
<!doctype html><meta charset="utf-8"><title>widget</title>
<pre id="o"></pre>
<script>
const log = [];
try { log.push("parent.document.title = " + parent.document.title); }
catch (e) { log.push("read parent: BLOCKED (" + e.name + ")"); }
try { top.location = "about:blank#hijacked"; log.push("top nav: ALLOWED ⚠"); }
catch (e) { log.push("top nav: BLOCKED"); }
try { window.open("about:blank"); log.push("popup: attempted"); }
catch (e) { log.push("popup: BLOCKED"); }
document.getElementById("o").textContent = log.join("\n");
</script>
EOF
cat > host.html <<'EOF'
<!doctype html><meta charset="utf-8"><title>host</title>
<h1>your-site.example</h1>
<h3>1) no sandbox</h3>
<iframe src="widget.html"></iframe>
<h3>2) sandbox="allow-scripts"</h3>
<iframe src="widget.html" sandbox="allow-scripts"></iframe>
<h3>3) sandbox="allow-scripts allow-same-origin" (footgun)</h3>
<iframe src="widget.html" sandbox="allow-scripts allow-same-origin"></iframe>
EOF
python3 -m http.server 5000 >/dev/null 2>&1 &
echo "open http://localhost:5000/host.html"
Quan sát
Mở http://localhost:5000/host.html và đọc ba khung:
- Không sandbox — đọc được
parent.document.title, thử đổi top: mọi thứ “ALLOWED”. allow-scripts— script chạy nhưng origin opaque → đọc cha BLOCKED, top nav BLOCKED.allow-scripts allow-same-origin(same-origin) — đọc cha ALLOWED trở lại; đây là cấu hình tự huỷ sandbox.
Đã chứng minh: sandbox cấm theo mặc định; kết hợp hai token nguy hiểm trên nội dung same-origin mở lại cửa.
Dọn dẹp
kill %1 2>/dev/null; rm -rf /tmp/sbx
Mẫu an toàn cho widget bên thứ ba
1. Cô lập trên origin riêng
Phục vụ nội dung không tin cậy từ origin khác (vd usercontent.your-cdn.example, không phải domain app). Khi đó dù lỡ thêm allow-same-origin, “same-origin” cũng không phải origin app của bạn. Đây là cách GitHub, Google… cô lập nội dung người dùng.
2. Cấp token tối thiểu
Widget cross-origin điển hình chỉ cần allow-scripts (và đôi khi allow-popups). Bắt đầu từ sandbox="" rồi thêm dần đến khi đủ chạy — không hơn.
3. Khoá thêm bằng allow (Permissions Policy) và csp
sandbox quản năng lực khung; thuộc tính allow quản quyền mạnh (camera, mic, geolocation, fullscreen):
<iframe src="https://widget.example/embed"
sandbox="allow-scripts allow-popups"
allow="camera 'none'; microphone 'none'; geolocation 'none'"
referrerpolicy="no-referrer"></iframe>
4. Giao tiếp an toàn qua postMessage
Khung sandbox không chạm được trang cha — đúng như mong muốn. Cần trao đổi dữ liệu thì dùng postMessage và luôn kiểm event.origin (xem Phần 14). Đừng “sửa” cô lập bằng allow-same-origin.
5. Phòng thủ phía bạn vẫn cần
Sandbox bảo vệ trang bạn khỏi khung. Nó không thay CSP (Phần 3) chống script độc trên chính origin bạn, cũng không thay framing protection (frame-ancestors, Phần 7) ngăn người khác nhúng bạn.
Checklist phòng tránh
- Nhúng nội dung không tin cậy → luôn dùng
sandbox, bắt đầu từ rỗng. - Cấp token tối thiểu; widget thường chỉ cần
allow-scripts. - Không kết hợp
allow-scripts+allow-same-origincho nội dung same-origin. - Phục vụ nội dung người dùng từ origin riêng.
- Tránh
allow-top-navigation; cần thì dùng bản-by-user-activation. - Khoá quyền mạnh bằng
allow="…"; đặtreferrerpolicy. - Trao đổi dữ liệu qua
postMessage+ kiểmorigin.
Bài tập / Exercises
1. Vì sao sandbox="allow-scripts allow-same-origin" lại nguy hiểm khi khung là same-origin, nhưng gần như vô hại khi khung cross-origin?
Lời giải
Same-origin: script giữ origin của bạn, với tới parent.document, gỡ luôn thuộc tính sandbox của chính nó rồi reload không giới hạn. Cross-origin: Same-Origin Policy vẫn chặn truy cập trang cha bất kể allow-same-origin, nên không escape được theo cách đó.
2. Một widget cần chạy JS và thỉnh thoảng mở cửa sổ thanh toán. Token tối thiểu là gì, và không nên thêm gì?
Lời giải
allow-scripts (+ allow-popups nếu cần cửa sổ mới). Không thêm allow-same-origin (nếu phục vụ same-origin) và không allow-top-navigation. Nếu thật cần điều hướng top theo click, dùng allow-top-navigation-by-user-activation.
3. Trong simulator, đặt nguồn cross-origin + chỉ allow-scripts, rồi bật allow-same-origin. Vì sao “đọc cha” vẫn bị chặn?
Lời giải
Vì khung ở origin khác với bạn. allow-same-origin chỉ giữ cho khung origin riêng của nó; SOP vẫn chặn nó đọc origin của bạn. Footgun chỉ kích hoạt khi origin khung trùng origin app.
Nâng cao:Trong simulator, tìm cấu hình mà widget chạy script, mở popup, nhưng không đọc được dữ liệu của bạn và không đổi được top — và viết ra thẻ <iframe> tương ứng.
Điểm chính
- iframe mặc định mạnh bất ngờ;
sandboxlật sang cấm-theo-mặc-định, cấp lại từng token. allow-scripts+allow-same-origintrên nội dung same-origin khiến khung tự huỷ sandbox — đừng bao giờ ghép.allow-top-navigationđể widget cướp cả tab — gần như không bao giờ cấp.- Cô lập nội dung không tin cậy trên origin riêng, cấp token tối thiểu, khoá quyền mạnh bằng
allow. - Giao tiếp qua
postMessage+ kiểmorigin, đừng dùngallow-same-originđể “tiện”.
Nguồn
- MDN —
<iframe sandbox>và<iframe allow>/ Permissions Policy. - HTML Standard — the sandboxing flag set (cảnh báo về
allow-scripts+allow-same-origin). - web.dev — Securely embedding third-party content.
- OWASP — HTML5 Security Cheat Sheet: iframe sandbox.
Series
Các phần bonus xếp trên mười phần lõi và nhánh nâng cao. Sợi chỉ từ Phần 1: mỗi origin bạn nhúng là một niềm tin trao đi — sandbox cho bạn cấp niềm tin đó theo từng phần, có chủ đích, mặc định bằng không.