HTML cơ bản cho tool web | Thẻ, attribute và vì sao phải escape trước khi render
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
innerHTMLrồ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ần | Ví dụ | Vai trò |
|---|---|---|
| Thẻ (element) | <p>, <a>, <input> | Cấu trúc + ngữ nghĩa |
| Attribute | href="...", value="...", class="..." | Metadata trên thẻ mở |
| Text node | chữ giữa <p> và </p> | Nội dung hiển thị |
<p class="note" title="Ghi chú">Xin chào An</p>
p= thẻclass,title= attributeXin 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:
- Template của bạn chứa thẻ cố định.
- Ô trống trong template chỉ nhận chuỗi đã escape hoặc binding text của framework.
- 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”.
| 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:
- Hiển thị tên user / comment như chữ thường → không muốn thẻ nào được parse.
- 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ụ:
| Input | Sau escape (rút gọn) | Trình duyệt hiện |
|---|---|---|
A < B | A < B | A < B (chữ) |
<script> | <script> | chữ <script> |
Tom & Jerry | Tom & Jerry | Tom & Jerry |
Nếu quên & → entity sau đó có thể “gãy” (< 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:
- Sink nào? Text / attribute / URL / email HTML?
- Ai viết thẻ? Chỉ template — user không được inject thẻ mới.
- Framework đã escape giúp chưa? JSX text thường có;
dangerouslySetInnerHTML, template string, email HTML thường không. - Có cần hiện mã nguồn? → escape bắt buộc.
- Có rich text? → chuyển sang sanitize, đừng chỉ
replace('<',''). - 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:
- Escape chuỗi mẫu bằng HTML Escape để điền vào khối code trên trang.
- Preview tách iframe/
srcdoccó sandbox nếu thật sự cần render. - 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àm | Dùng tool thế nào |
|---|---|
Xem < thành < | HTML Escape → Escape |
| Đọc entity trong log / ticket | Unescape trên cùng tool, rồi escape lại trước khi render |
| Soạn test case cho QA | Tạ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/iddo user chọn là vô hại (vẫn có thể phá CSS hoặc selector). - Copy snippet Stack Overflow dùng
innerHTMLcho comment form.
Khi nào đọc bài tiếp theo
- Đã rõ text vs attribute vs URL → Escape vs sanitize cho form/CMS
- Cần thao tác entity ngay → HTML Escape trên trình duyệt
- Làm việc với chuỗi escape kiểu
\ntrong code → khác HTML entity; xem bài escape sequence nếu bạn đang debug log/regex
Tóm tắt mang đi
- HTML = thẻ + attribute + text — đừng gộp thành “một chuỗi”.
- Escape theo ngữ cảnh chèn, không theo thói quen “luôn thay <”.
- URL attribute cần allowlist scheme.
- Tool escape local để debug và viết test — không thay kiến trúc bảo mật.
- 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.