HTML cơ bản cho tool web | Thẻ, attribute và vì sao phải escape trước khi render

(Cập nhật: 19 tháng 7, 2026 ) HTML thẻ HTML attribute HTML escape XSS web tool frontend
Kết luận

Trước khi escape, hãy biết bạn đang chèn vào đâu. Thẻ tạo khung; attribute mang giá trị; text node là chữ người đọc thấy. User input vào text → escape (hoặc textContent / JSX text). User input vào attribute → escape cả nháy. User input vào href/src → allowlist scheme, không chỉ escape. Debug entity với HTML Escape Kawa (local) — tool giúp nhìn < / &, không thay review template.

Nhiều sinh viên và freelancer Việt Nam nhảy thẳng vào “phải escape để chống XSS” mà chưa trả lời được câu hỏi đơn giản hơn: chuỗi này đang nằm ở vị trí nào trong HTML? Cùng một nickname An <b>cool</b> an toàn trong một chỗ, nguy hiểm ở chỗ khác — vì parser HTML xử lý thẻ, attribute và text khác nhau.

Bài này là nền HTML cho người làm tool web / form / trang demo: thẻ, attribute, text node, và vì sao bước đó phải đứng trước cheat sheet escape/sanitize. Nếu bạn đã rõ ngữ cảnh và cần chọn escape vs sanitize cho CMS, đọc tiếp Escape vs sanitize.

Ai nên đọc bài này

  • Sinh viên lần đầu ghép user input vào trang HTML tĩnh hoặc Astro/React
  • Freelancer nhận form comment, bảng admin, trang “preview HTML” từ khách
  • Ai hay paste payload vào innerHTML rồi mới hỏi “sao bị XSS?”

Bạn sẽ có: mô hình 3 lớp (thẻ / attribute / text), bảng ngữ cảnh, checklist trước khi mở tool escape, và 2 case study ngắn.


HTML tối thiểu mà tool web cần

Trình duyệt không “đọc văn bản tự do” — nó parse thành cây DOM. Ba thứ hay lẫn:

Thành phầnVí dụVai trò
Thẻ (element)<p>, <a>, <input>Cấu trúc + ngữ nghĩa
Attributehref="...", value="...", class="..."Metadata trên thẻ mở
Text nodechữ giữa <p></p>Nội dung hiển thị
<p class="note" title="Ghi chú">Xin chào An</p>
  • p = thẻ
  • class, title = attribute
  • Xin chào An = text

Khi bạn làm tool web (escape, formatter, preview), gần như mọi bug đều là: đưa dữ liệu user vào nhầm lớp.

Thẻ bạn viết vs dữ liệu user

Bạn quyết định thẻ nào tồn tại: form, bảng, layout. User chỉ được phép đóng góp dữ liệu — không được quyết định “có thêm thẻ script không”.

Quy tắc thực dụng:

  1. Template của bạn chứa thẻ cố định.
  2. Ô trống trong template chỉ nhận chuỗi đã escape hoặc binding text của framework.
  3. Nếu product cho phép HTML giàu (bold, link) → đó là bài sanitize whitelist, không phải “escape một lần toàn cục”.

Attribute: chỗ hay bị bỏ quên

Attribute không phải text. Parser đọc:

<input value="USER_HERE">
<a href="USER_HERE">link</a>

Breakout bằng dấu nháy

Giả sử nickname = x" onfocus="alert(1).

Nếu bạn ghép:

<input value="x" onfocus="alert(1)">

Bạn vừa tạo thêm attribute onfocus. Chỉ thay < > không đủ — phải escape " (và thường &) trong ngữ cảnh attribute.

URL attribute khác text

href / src / action còn nguy hiểm hơn vì scheme:

  • An toàn hơn khi allowlist: https:, http:, mailto: (tùy product)
  • Nguy hiểm: javascript:, data:text/html,...

Escape entity không biến javascript:alert(1) thành link an toàn. Đây là lý do bài escape/sanitize tách riêng “URL context”.

Cùng chuỗi user — 3 ngữ cảnh khác nhau
Chỗ chèn Rủi ro chính Hướng xử lý
Text giữa thẻ Thẻ/script được parse nếu dùng innerHTML thô Escape hoặc textContent / JSX text
Attribute trong nháy Thoát nháy → thêm attribute/event Escape & " ' < > theo rule attribute
href / src javascript: / data: chạy mã Allowlist scheme + validate URL, không chỉ escape

Text node và “muốn hiện mã HTML trên trang”

Hai nhu cầu hay gặp trong tool / blog kỹ thuật:

  1. Hiển thị tên user / comment như chữ thường → không muốn thẻ nào được parse.
  2. Hiển thị ví dụ mã <div class="box"> trên trang hướng dẫn → người đọc phải thấy ký tự <, không phải một hộp thật.

Cả hai đều cần escape trước khi đưa vào HTML thô. Khác nhau ở product intent, không ở “có XSS hay không”.

Ví dụ:

InputSau escape (rút gọn)Trình duyệt hiện
A < BA &lt; BA < B (chữ)
<script>&lt;script&gt;chữ &lt;script&gt;
Tom & JerryTom &amp; JerryTom & Jerry

Nếu quên & → entity sau đó có thể “gãy” (&lt; thành text lạ hoặc parse sai).

Muốn xem nhanh chuỗi mẫu thành entity gì, mở HTML Escape / Unescape, dán payload, Escape — chạy trên trình duyệt, không upload server. Dùng để đối chiếu với output template của bạn, không phải để “vá production bằng tay từng dòng”.


Checklist trước khi mở tool escape

Dành cho PR form / trang admin / preview:

  1. Sink nào? Text / attribute / URL / email HTML?
  2. Ai viết thẻ? Chỉ template — user không được inject thẻ mới.
  3. Framework đã escape giúp chưa? JSX text thường có; dangerouslySetInnerHTML, template string, email HTML thường không.
  4. Có cần hiện mã nguồn? → escape bắt buộc.
  5. Có rich text? → chuyển sang sanitize, đừng chỉ replace('<','').
  6. Thử 3 payload tối thiểu:
    • <img src=x onerror=alert(1)>
    • " onmouseover="alert(1)
    • javascript:alert(1) (trên href)

Payload (1) kiểm text/innerHTML. Payload (2) kiểm attribute. Payload (3) kiểm URL. Nếu team chỉ test (1), attribute/URL vẫn thủng.


Case study 1 — Form “tên hiển thị” trên landing freelance

Khách muốn trang cảm ơn: Cảm ơn, {name}!. Dev junior viết:

el.innerHTML = `Cảm ơn, ${name}!`;

name lấy từ query ?name=. QA nhập An<script>alert(1)</script> → XSS. Sửa đúng:

  • el.textContent = 'Cảm ơn, ' + name
  • hoặc escape rồi mới gán HTML
  • hoặc template engine / React text binding

Bài học: lỗi không nằm ở “quên thư viện XSS” — nằm ở việc chọn innerHTML cho dữ liệu vốn là text node.

Case study 2 — Tool preview HTML trong bài lab trường

Sinh viên làm trang “dán HTML → xem trước”. Hai ô: nguồn và preview. Ô nguồn phải hiện thẻ như chữ (escape). Ô preview cố ý render (chỉ trên data mẫu local, không nhúng user lạ trên production).

Nhầm lẫn phổ biến: escape cả hai ô, hoặc không escape ô nguồn → layout trang lab bị “vỡ” vì thẻ trong bài mẫu được parse vào DOM thật. Workflow tốt:

  1. Escape chuỗi mẫu bằng HTML Escape để điền vào khối code trên trang.
  2. Preview tách iframe/srcdoc có sandbox nếu thật sự cần render.
  3. Không copy output preview vào CMS công khai nếu chưa sanitize.

Map sang công cụ Kawa (không phải giải pháp duy nhất)

Việc bạn đang làmDùng tool thế nào
Xem < thành &lt;HTML Escape → Escape
Đọc entity trong log / ticketUnescape trên cùng tool, rồi escape lại trước khi render
Soạn test case cho QATạo bảng input → entity kỳ vọng, gắn vào PR

Tool không thay:

  • Binding an toàn của React/Vue
  • Sanitize whitelist cho CMS
  • CSP, HttpOnly cookie, review dangerouslySetInnerHTML

Nó là kính lúp khi bạn đã hiểu thẻ/attribute — đúng với góc của bài này.


Lỗi thường gặp khi mới học HTML + escape

  • Escape “một lần lúc lưu DB”, rồi tin mọi kênh xuất đều an toàn.
  • Chỉ thay < > trong attribute có nháy.
  • Unescape payload debug rồi nhét lại innerHTML.
  • Coi class / id do user chọn là vô hại (vẫn có thể phá CSS hoặc selector).
  • Copy snippet Stack Overflow dùng innerHTML cho comment form.

Khi nào đọc bài tiếp theo


Tóm tắt mang đi

  1. HTML = thẻ + attribute + text — đừng gộp thành “một chuỗi”.
  2. Escape theo ngữ cảnh chèn, không theo thói quen “luôn thay <”.
  3. URL attribute cần allowlist scheme.
  4. Tool escape local để debug và viết test — không thay kiến trúc bảo mật.
  5. Form plain text: ưu tiên text binding; CMS giàu: sanitize có chủ đích.

Bookmark bài này nếu bạn hay nhận ticket “hiện sai / XSS” mà chưa biết nên sửa template, attribute hay URL trước.