Nguyên tắc gốc: không bao giờ ghép dữ liệu người dùng thành HTML rồi cho trình duyệt phân tích lại.
Trường hợp mặc định — hiển thị dạng văn bản. React đã tự escape khi render {comment}, nên <img src=x onerror=alert(1)> chỉ hiện ra như chuỗi ký tự. Chỉ khi dùng dangerouslySetInnerHTML, innerHTML hoặc insertAdjacentHTML thì lỗ hổng mới mở ra.
Nếu bình luận cần định dạng (in đậm, link). Có hai cách an toàn:
- Nhận Markdown, chuyển sang cấu trúc dữ liệu rồi render bằng component React — không đi qua chuỗi HTML.
- Nếu buộc phải nhận HTML, sanitize bằng thư viện đã kiểm chứng (DOMPurify) với allowlist thẻ/thuộc tính. Đừng tự viết hàm lọc bằng regex: bộ phân tích HTML của trình duyệt rất khoan dung, biến thể qua thẻ chưa đóng, thuộc tính không dấu nháy hoặc mã hoá ký tự nhiều tầng luôn nhiều hơn số case regex bắt được.
Các bề mặt hay bị bỏ sót:
- URL do người dùng nhập:
<a href={url}>vớijavascript:...vẫn chạy khi bấm. Chỉ cho phéphttp:/https:/mailto:. - Sanitize ở client là chưa đủ — API có thể bị gọi trực tiếp, nên server phải lọc trước khi lưu. An toàn nhất là lưu nguyên bản, escape/sanitize lúc xuất, và làm ở cả hai phía.
- Cấu hình
dangerouslySetInnerHTMLtừ dữ liệu server cũng là dữ liệu không tin cậy nếu nội dung đó bắt nguồn từ người dùng khác.
Lớp phòng thủ cuối là Content-Security-Policy: chặn inline script và nguồn script lạ, để một lỗ hổng lọt lưới cũng không thực thi được. CSP là lớp bổ sung, không thay thế việc escape đúng chỗ.