Đang tải…
Đang tải…
Chặn thẻ script là phần dễ. Phần khó là iframe, thuộc tính srcset, và scheme javascript: nằm trong href mà bạn quên mất.

Bài viết trên site này lưu dưới dạng HTML, do trình soạn thảo trong CMS sinh ra, rồi đổ vào dangerouslySetInnerHTML lúc render. Cái tên prop đó dài và khó chịu đúng như chủ đích của React.
Giữa hai bước là một lượt sanitize. Đây là cấu hình của nó, và lý do từng dòng có mặt.
sanitizeHtml(content, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img", "iframe"]),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ["src", "alt", "width", "height"],
iframe: ["src", "width", "height", "allowfullscreen", "frameborder", "allow"],
},
allowedIframeHostnames: ["www.youtube.com", "www.youtube-nocookie.com", "player.vimeo.com"],
allowIframeRelativeUrls: false,
allowedSchemes: ["http", "https", "mailto"],
allowedSchemesByTag: { img: ["http", "https", "data"] },
});
Điểm quan trọng nhất nằm ở chữ allowed lặp đi lặp lại. Cách tiếp cận ngược lại là liệt kê những gì cấm, và nó thua ngay từ đầu: bạn phải đoán trước mọi thứ nguy hiểm, còn bên tấn công chỉ cần tìm ra một thứ bạn chưa nghĩ tới. Danh sách cho phép thì mặc định từ chối, và cái giá phải trả là thỉnh thoảng bạn phát hiện một thẻ hợp lệ bị nuốt mất.
Danh sách mặc định của thư viện có 70 thẻ. pre, code, table, blockquote, figure đều nằm sẵn trong đó, nên bài kỹ thuật viết bằng chúng thì không phải khai báo gì thêm. img và iframe thì không, và đó chính là lý do hai thẻ đó phải được nối thêm vào một cách tường minh.
Điều đáng nhớ về kiểu lọc này: khi một thẻ bị loại, không có lỗi nào được ném ra. Nội dung chỉ đơn giản là không xuất hiện. Nên lần đầu dùng một thẻ mới trong bài, hãy mở trang thật ra xem, đừng tin vào việc nó lưu thành công.
Cho phép iframe nghe đã thấy liều, và đúng là liều nếu chỉ dừng ở việc thêm nó vào allowedTags. Một iframe không giới hạn nguồn có thể nhúng trang đăng nhập giả, trang lừa đảo, hoặc bất cứ thứ gì mà người đọc sẽ tưởng là của bạn vì nó nằm trên domain của bạn.
allowedIframeHostnames thu phạm vi về đúng ba host phục vụ nhúng video. Mọi src khác bị loại bỏ.
allowIframeRelativeUrls: false bịt đường vòng còn lại. Không có nó, một src tương đối kiểu /admin sẽ lọt qua bộ lọc hostname, vì nó không có hostname nào để kiểm tra.
Thuộc tính có thể nằm trong danh sách cho phép mà giá trị bên trong vẫn độc. href là thuộc tính hoàn toàn bình thường; href="javascript:..." thì không.
allowedSchemes giới hạn còn http, https và mailto. Thế là javascript: và data: ra khỏi cuộc chơi ở mọi thẻ.
Rồi allowedSchemesByTag mở lại data: cho riêng img. Ảnh nhúng inline là thứ hợp lệ và trình soạn thảo có sinh ra, còn data: trong một href thì là đường dẫn tới trang HTML tự chế chạy trên origin của bạn. Cùng một scheme, hai mức rủi ro khác hẳn nhau, và cấu hình phải phân biệt được điều đó.
Chỗ này có một lựa chọn thật, và mình không nghĩ có đáp án đúng cho mọi trường hợp.
Sanitize lúc ghi thì chạy đúng một lần cho mỗi bài. Rẻ hơn, và database chỉ chứa dữ liệu đã sạch. Cái mất là bạn không sửa lại được: hôm nào phát hiện quy tắc cũ có lỗ hổng, những bài đã lưu vẫn giữ nguyên phần độc, và bạn phải chạy migration trên toàn bộ nội dung.
Sanitize lúc đọc thì tốn CPU mỗi lần render, nhưng quy tắc mới có hiệu lực ngay cho mọi bài cũ. Site này chọn cách thứ hai, và cái giá gần như bằng không vì trang bài viết được prerender: lượt sanitize chạy lúc build, không phải lúc có người đọc.
Nếu nội dung của bạn render động theo từng request thì phép tính đổi khác. Còn nếu nội dung do người dùng gửi lên chứ không phải do admin viết, thì làm cả hai, và đừng tiếc CPU.
Chưa có bình luận nào — hãy là người đầu tiên!