Cả hai đều parse chuỗi thành HTML thật rồi gắn vào DOM. Mọi thẻ, thuộc tính sự kiện trong chuỗi đều sống. React mặc định escape khi bạn render {value}, nên dangerouslySetInnerHTML chính là chỗ bạn tự tay tắt lớp bảo vệ đó — tên prop dài dòng là cố ý.
Lưu ý: <script> chèn qua innerHTML không chạy, nên nhiều người tưởng là an toàn. Nhưng handler thì chạy:
<img src=x onerror="fetch('/api/me').then(r=>r.text()).then(send)">
<svg><animate onbegin="..."></svg>Cách làm đúng:
1. Ưu tiên không render HTML. Nếu chỉ cần in chữ, dùng textContent hoặc {value} của React.
2. Nếu cần rich text (bình luận, mô tả từ CMS), sanitize bằng DOMPurify — thư viện đã qua nhiều vòng kiểm định, đừng tự viết regex lọc thẻ.
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['p', 'b', 'i', 'a', 'ul', 'li', 'code'],
ALLOWED_ATTR: ['href'],
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />3. Sanitize ở thời điểm render, không phải lúc lưu — vì cùng một chuỗi lưu trong DB có thể được dùng ở nhiều ngữ cảnh khác nhau, và bản DOMPurify sẽ được vá theo thời gian.
4. Whitelist thẻ/thuộc tính thay vì blacklist. Blacklist luôn thiếu.