Escape vs sanitize: XSS từ form comment / CMS freelance — khi nào cần cái nào
Escape ≠ sanitize. Form comment / tên hiển thị plain text → escape (hoặc text binding framework) trước khi vào HTML. CMS / WYSIWYG cho phép in đậm, link → sanitize whitelist (DOMPurify…), không chỉ replace('<',''). XSS không chỉ innerHTML: attribute và javascript: URL cũng đủ. Debug entity nhanh với HTML escape Kawa (local, không gửi server) — tool không thay CSP hay review dangerouslySetInnerHTML.
Nhiều team freelance Việt Nam coi XSS là “nhớ đừng dùng innerHTML”. Rồi ticket tới: tên user nằm trong attribute, bio dán vào href, hoặc CMS WordPress/Strapi cho HTML thô trong bài — không hề gọi innerHTML trong React, vẫn XSS.
Escaping là theo ngữ cảnh. Cùng một chuỗi cần encode khác nhau tùy chỗ chèn. Chỉ thay < > rồi tuyên bố xong — thường chưa xong.
Bài viết này giúp bạn
- Phân biệt escape (hiển thị an toàn) và sanitize (giữ HTML hẹp)
- Map sink: text / attribute / URL / email
- Case study form comment và CMS freelance
- Bảng payload QA tối thiểu
- Thử entity trên HTML escape/unescape trước khi sửa template
Escape vs sanitize — chọn theo ý product
| Tiêu chí | HTML escape | HTML sanitize |
|---|---|---|
| Mục tiêu | Mọi thẻ thành chữ — không thực thi | Cho phép subset HTML an toàn |
| Ví dụ output | <b>hi</b> → <b>hi</b> | <b>hi</b> giữ; <script> bị loại |
| Phù hợp | Comment plain, username, log, code sample | CMS, rich text, markdown→HTML |
| Sai khi | Dùng escape rồi expect in đậm vẫn hiện | Sanitize kém + tin user HTML 100% |
| Thư viện điển hình | Framework auto-escape, hàm escapeHtml | DOMPurify, sanitize-html, Bleach… |
Quy tắc vàng: Product chỉ cần chữ → escape. Product cần định dạng → sanitize theo whitelist, rồi vẫn cẩn thận URL/scheme trong thẻ được phép.
Ngữ cảnh — mỗi sink một rule
1. Nội dung text giữa thẻ
Escape tối thiểu & < > để <script> thành chữ nhìn thấy, không chạy.
2. Giá trị attribute (có quote)
Escape thêm " ’` — nếu không, payload kiểu:
"><img src=x onerror=alert(1)>
phá value="..." và gắn handler.
3. URL (href, src)
Entity-escape không đủ. Cần allowlist scheme (https:, mailto:…). javascript: và nhiều data: vẫn nguy hiểm dù đã escape HTML một phần.
4. Trong <script> / JSON nhúng
Dùng serialization JSON đúng cách — HTML escape đơn thuần là sai tool.
5. CSS do user kiểm soát
Tránh cho raw CSS (url(), expression legacy…). Prefer không nhận.
Hàm escape điển hình cho text + attribute có quote:
function escapeHtml(text) {
return String(text)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Bảng entity:
| Ký tự | Sau escape |
|---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
Thử ngay trên trình duyệt (không upload):
🛡️ Escape HTML ngay tại đây
Hoặc mở HTML Escape, dán payload QA, xem dạng encode kỳ vọng — rồi đối chiếu output template của bạn.
Case study 1: Form comment freelance — plain text
Yêu cầu khách: trang portfolio + form “Để lại lời nhắn”. Stack: Next/Astro + API lưu Postgres, render danh sách comment bằng JSX {c.body}.
Đúng: coi body là plain text — React escape text children → đủ cho sink này. Server cũng validate độ dài, reject null byte nếu paranoid.
Sai phổ biến:
- Muốn “cho phép xuống dòng đẹp” → chuyển
\nthành<br>bằng cách ghép HTML string rồidangerouslySetInnerHTMLmà không escape trước → XSS nếu user gửi<img onerror=...>. - Escape lúc
INSERT, unescape lúc hiển thị “cho đẹp” → vòng tròn vô nghĩa + bug&amp;. - Copy snippet PHP/
echo $_POSTtừ tutorial cũ vào API Node “tạm”.
Fix an toàn cho xuống dòng: escape toàn bộ, rồi thay \n → <br> trên chuỗi đã escape, hoặc dùng CSS white-space: pre-wrap trên text node — không mở HTML thô.
Case study 2: CMS / editor giàu — cần sanitize
Yêu cầu: blog khách hàng, editor TipTap/CKEditor, lưu HTML. Freelancer bật “cho HTML” vì marketing muốn nhúng YouTube.
Nếu chỉ escape: mọi thẻ thành chữ — feature rich text chết.
Nếu không sanitize: stored XSS trong bài — visitor admin bị cookie/session tấn khi xem preview.
Pipeline đúng hướng:
- Lưu HTML đã qua sanitizer (server bắt buộc; client chỉ UX).
- Whitelist:
p, br, strong, em, a[href], ul, ol, li, img[src]… — bỏscript,iframetrừ khi có CSP + domain allowlist rõ. - Với
a[href]/img[src]: chỉhttps:(và relative nếu tin tưởng origin). - Markdown → HTML: render rồi sanitize, đừng tin markdown “an toàn mặc định”.
- CSP bổ sung — defense in depth, không thay sanitize.
WordPress/plugin cũ hay để shortcode/HTML tùy ý: audit trước khi bàn giao hosting shared — XSS stored trên site khách = ticket bảo mật về đúng freelancer.
Framework giúp gì — và không giúp gì
| Sink | Phòng thủ |
|---|---|
| Text node (JSX/template auto-escape) | Escape / binding text |
| Attribute có quote | Escape quote + luôn quote attribute |
href / src user | Allowlist scheme + encode |
| HTML email | Escape chặt + ít thẻ |
| Markdown → HTML | Sanitizer sau render |
React: an toàn hơn với {user.name}; vẫn nguy hiểm với dangerouslySetInnerHTML, tự build HTML string, href={userUrl} không check scheme, email HTML ngoài React.
Jinja / EJS / Liquid / PHP: auto-escape mặc định khác nhau; tắt nhầm một partial = lỗ. Filter JS-escape ≠ HTML-escape.
Payload tối thiểu cho QA
"><img src=x onerror=alert(1)>
'><script>alert(1)</script>
javascript:alert(1)
{{constructor.constructor('alert(1)')()}}
Không phải payload nào cũng hit mọi stack — mục tiêu là probe attribute breakout, text injection, URL scheme riêng. Suite chỉ grep <script> trong HTML text sẽ bỏ sót attribute XSS.
| Nơi test | Kỳ vọng |
|---|---|
| Ô comment plain | Thấy entity / chữ, không alert |
Profile title="..." | Không breakout attribute |
| Link “website” user | Chặn javascript: |
| CMS preview admin | Script trong bài không chạy |
Lỗi tinh vi hay gặp
| Lỗi | Hiện tượng |
|---|---|
| Escape lúc ghi DB | Double-escape; kênh JSON/API vẫn “raw” sai chỗ |
Chỉ escape < > | Attribute XSS / URL scheme |
| Unescape rồi innerHTML | Tự mở lại XSS |
| Tin “CMS đã xử lý” | Plugin/theme inject HTML thô |
| Encode UTF-8 tiếng Việt sai | Entity lạ hoặc cắt chuỗi — luôn UTF-8 end-to-end |
Tiếng Việt trong comment (Đà Nẵng, emoji) không phải XSS — nhưng encoding lệch dễ làm QA nghĩ “escape làm hỏng chữ” rồi tắt escape. Giữ UTF-8; escape sau khi đã có Unicode đúng.
Cách dùng công cụ trên Kawa
- Mở HTML Escape / Unescape
- Dán payload hoặc đoạn template nghi ngờ
- Escape → copy entity kỳ vọng; Unescape chỉ để đọc dữ liệu đã encode
- Đối chiếu với HTML thật trong DevTools (View Source / Elements)
Chạy trong trình duyệt — phù hợp log/comment sample có PII nhẹ hơn upload lên “XSS tester” lạ. Không dùng tool thay thư viện sanitizer production hay CSP.
Tóm tắt mang đi
- Plain text user content → escape theo sink.
- Rich HTML/CMS → sanitize whitelist + kiểm URL.
- XSS ≠ chỉ
innerHTML; attribute và scheme cũng đủ. - Bookmark HTML escape local khi debug entity và viết test case.