jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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: sandbox là 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

TokenCấp quyền gìMức nguy hiểm
allow-scriptschạy JavaScriptcần cho hầu hết widget
allow-same-origingiữ nguyên origin (không opaque)cẩn trọng — xem phần dưới
allow-formsgửi formtrung bình
allow-popupswindow.opentrung bình (popup kế thừa sandbox)
allow-top-navigationđổi top.locationgần như không bao giờ cấp
allow-modalsalert/confirm/printthấp (phiền/scareware)
allow-downloadskhởi động tải filetrung 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) 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-scriptsallow-same-origin cho nội dung same-origin mà bạn không hoàn toàn tin. Nếu cần script 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:

  1. Không sandbox — đọc được parent.document.title, thử đổi top: mọi thứ “ALLOWED”.
  2. allow-scripts — script chạy nhưng origin opaque → đọc cha BLOCKED, top nav BLOCKED.
  3. 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 postMessageluô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

  1. Nhúng nội dung không tin cậy → luôn dùng sandbox, bắt đầu từ rỗng.
  2. Cấp token tối thiểu; widget thường chỉ cần allow-scripts.
  3. Không kết hợp allow-scripts + allow-same-origin cho nội dung same-origin.
  4. Phục vụ nội dung người dùng từ origin riêng.
  5. Tránh allow-top-navigation; cần thì dùng bản -by-user-activation.
  6. Khoá quyền mạnh bằng allow="…"; đặt referrerpolicy.
  7. Trao đổi dữ liệu qua postMessage + kiểm origin.

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ờ; sandbox lật sang cấm-theo-mặc-định, cấp lại từng token.
  • allow-scripts + allow-same-origin trê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ểm origin, đừng dùng allow-same-origin để “tiện”.

Nguồn


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.