Xem trước an toàn một trang web bên ngoài — triển khai proxy + sanitize + iframe sandbox
- engineering
- security
- nextjs
- iframe
Tóm tắt bài viết
- Khi tải một trang web bên ngoài vào iframe trên chính domain của mình, có nguy cơ JS bên ngoài có thể can thiệp vào phiên làm việc (session) của mình
- Giải pháp là phòng thủ nhiều lớp: "proxy có chống SSRF + loại bỏ JS bên ngoài + CSP hạn chế + iframe sandbox"
- Sandbox nếu quá chặt sẽ chặn luôn cả script của chính mình. Cũng có cạm bẫy là quên thêm
allow-scriptskhiến tính năng không hoạt động
Tính năng A/B test của HeatMapX có chức năng "xem trước giao diện sau khi thay đổi (Variant B) đã được áp dụng lên trang đích". Về mặt kỹ thuật, đây là việc "hiển thị an toàn trang web bên ngoài của người dùng bên trong một iframe trên domain của chính mình" — một xử lý khá tinh tế. Sau đây là phần chia sẻ về thiết kế cũng như những cạm bẫy thực tế đã gặp phải.
Vì sao cách triển khai đơn giản lại nguy hiểm
Nếu lấy nguyên HTML của trang web bên ngoài rồi phân phối trực tiếp trên domain của mình (ví dụ: heatmapx.com), thì các thẻ <script> hay thuộc tính onclick= chứa trong HTML đó sẽ được thực thi với quyền của chính domain mình. Khi đó, có nguy cơ Cookie hay phiên đăng nhập bị đọc trộm. Đây là điều bắt buộc phải tránh.
Cấu trúc phòng thủ nhiều lớp
Vì vậy, chúng tôi đã xếp chồng các lớp sau.
1. Proxy có chống SSRF
Kiểm tra URL cần lấy dữ liệu, từ chối mọi giao thức khác ngoài http/https. Ngoài ra còn chặn localhost, các dải IP nội bộ (private IP), và các endpoint metadata của cloud (như 169.254.169.254). Không tự động theo redirect, mà dùng redirect: 'manual' để kiểm tra lại từng bước một, nhằm ngăn việc bị dẫn hướng vào mạng nội bộ.
2. Loại bỏ JS bên ngoài (sanitize)
Từ HTML đã lấy về, loại bỏ thẻ <script>, các handler inline on*=, và scheme javascript:. Vì trong bản xem trước không cần thiết phải chạy JS của trang khách hàng (việc SPA không thể tái hiện hoàn toàn là một sự đánh đổi đã biết trước).
3. CSP hạn chế và base href
Chèn <base href> ngay sau <head> để giải quyết đường dẫn tương đối cho ảnh và CSS, đồng thời gắn CSP hạn chế "không cho phép chạy JS bên ngoài" vào response. Ảnh và CSS vẫn được cho phép để phục vụ hiển thị.
4. iframe sandbox
Cuối cùng, gắn thuộc tính sandbox vào iframe ở phía hiển thị để cô lập.
Cạm bẫy đã gặp phải: sandbox quá chặt
Bản xem trước hoạt động bằng cách chèn một đoạn "script tự viết nhỏ để áp dụng thay đổi (changeSet)" vào HTML đã lấy về, rồi chạy nó bên trong iframe để thay đổi giao diện. Tuy nhiên, thuộc tính sandbox của iframe dùng cho xem trước chỉ có allow-same-origin, và thiếu mất allow-scripts.
Kết quả là script tự viết cũng không được thực thi, dẫn đến lỗi thay đổi không được áp dụng mà trang gốc vẫn hiển thị nguyên trạng. Trong khi đó, iframe phía trình chỉnh sửa lại hoạt động đúng với allow-scripts allow-same-origin, nên nguyên nhân chính là sự không đồng nhất trong cấu hình.
Cách sửa rất đơn giản: chỉ cần đồng bộ phía xem trước cũng dùng allow-scripts allow-same-origin. Đồng thời, chúng tôi cũng đã bổ sung một bài test hồi quy (regression test) để kiểm tra thuộc tính sandbox của iframe.
sandbox="allow-same-origin" → sandbox="allow-scripts allow-same-origin"
Bài học
- Việc nhúng nội dung bên ngoài cần được suy nghĩ theo hướng "phòng thủ nhiều lớp". Không nên phụ thuộc vào một biện pháp duy nhất.
sandboxkhông phải là "cứ gắn vào là an toàn", mà điều quan trọng là chỉ cho phép chính xác những quyền cần thiết. Nếu quá chặt sẽ chặn luôn cả chức năng của chính mình.- Với "hai luồng xử lý cùng một việc" (trình chỉnh sửa và bản xem trước), phải luôn kiểm tra xem các thuộc tính bảo mật đã được đồng bộ hay chưa. Nếu chỉ sửa một bên thì bên còn lại sẽ bị hỏng.
Kết luận
Việc xem trước an toàn trang web bên ngoài có thể được thực hiện bằng phòng thủ nhiều lớp gồm: chống SSRF, loại bỏ JS bên ngoài, CSP hạn chế và iframe sandbox. Và quyền hạn của sandbox, dù thiếu hay thừa, đều có thể gây ra vấn đề. Việc quên thêm allow-scripts lần này chính là một ví dụ điển hình cho điều đó.