Escape vs sanitize: XSS từ form comment / CMS freelance — khi nào cần cái nào

(Cập nhật: 19 tháng 7, 2026 ) HTML escape XSS sanitize CMS bảo mật freelancer
Kết luận

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

Escape và sanitize khác nhau ở đâu
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>&lt;b&gt;hi&lt;/b&gt; <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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

Bảng entity:

Ký tự → entity
Ký tự Sau escape
< &lt;
> &gt;
& &amp;
" &quot;
' &#39;

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:

  1. Muốn “cho phép xuống dòng đẹp” → chuyển \n thành <br> bằng cách ghép HTML string rồi dangerouslySetInnerHTML mà không escape trước → XSS nếu user gửi <img onerror=...>.
  2. Escape lúc INSERT, unescape lúc hiển thị “cho đẹp” → vòng tròn vô nghĩa + bug &amp;amp;.
  3. Copy snippet PHP/echo $_POST từ 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:

  1. Lưu HTML đã qua sanitizer (server bắt buộc; client chỉ UX).
  2. Whitelist: p, br, strong, em, a[href], ul, ol, li, img[src]… — bỏ script, iframe trừ khi có CSP + domain allowlist rõ.
  3. Với a[href] / img[src]: chỉ https: (và relative nếu tin tưởng origin).
  4. Markdown → HTML: render rồi sanitize, đừng tin markdown “an toàn mặc định”.
  5. 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ì

SinkPhòng thủ
Text node (JSX/template auto-escape)Escape / binding text
Attribute có quoteEscape quote + luôn quote attribute
href / src userAllowlist scheme + encode
HTML emailEscape chặt + ít thẻ
Markdown → HTMLSanitizer 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 testKỳ vọng
Ô comment plainThấy entity / chữ, không alert
Profile title="..."Không breakout attribute
Link “website” userChặn javascript:
CMS preview adminScript trong bài không chạy

Lỗi tinh vi hay gặp

LỗiHiện tượng
Escape lúc ghi DBDouble-escape; kênh JSON/API vẫn “raw” sai chỗ
Chỉ escape < >Attribute XSS / URL scheme
Unescape rồi innerHTMLTự mở lại XSS
Tin “CMS đã xử lý”Plugin/theme inject HTML thô
Encode UTF-8 tiếng Việt saiEntity 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

  1. Mở HTML Escape / Unescape
  2. Dán payload hoặc đoạn template nghi ngờ
  3. Escape → copy entity kỳ vọng; Unescape chỉ để đọc dữ liệu đã encode
  4. Đố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.

Liên kết liên quan