Browser & Network
Web Security
XSS, CSRF, CSP, and secure coding practices for frontend development
Web Security
Web security is critical for protecting users and data. This covers common vulnerabilities and how to prevent them.
Common Vulnerabilities
XSS (Cross-Site Scripting)
XSS allows attackers to inject malicious scripts into web pages.
Types of XSS
XSS Types:
├── Stored XSS: Script stored in database, served to all users
├── Reflected XSS: Script in URL parameters, reflected in response
└── DOM-based XSS: Script manipulates DOM directly in browserVulnerable Code
// DANGEROUS - Direct HTML insertion
element.innerHTML = userInput;
document.write(userInput);
element.outerHTML = userInput;
// DANGEROUS - URL without validation
location.href = userInput;
window.open(userInput);
// DANGEROUS - Eval and similar
eval(userInput);
new Function(userInput);
setTimeout(userInput, 0);Prevention
// Use textContent instead of innerHTML
element.textContent = userInput;
// Escape HTML entities
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Sanitize HTML if you must allow it
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
// React automatically escapes
function SafeComponent({ userInput }) {
return <div>{userInput}</div>; // Safe - auto-escaped
}
// DANGEROUS in React - avoid dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // Vulnerable
// Validate URLs
function isValidUrl(url: string): boolean {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}CSRF (Cross-Site Request Forgery)
CSRF tricks users into performing unwanted actions on authenticated sites.
Attack Example
<!-- Attacker's site -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- Or hidden form -->
<form action="https://bank.com/transfer" method="POST" id="evil-form">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('evil-form').submit();</script>Prevention
// 1. Use CSRF tokens
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
});
// 2. Use SameSite cookies
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
// 3. Verify Origin/Referer headers (server-side)
// 4. Use POST for state-changing operations
// GET /api/delete/123 // DANGEROUS
// POST /api/items/123/delete // BetterClickjacking
Clickjacking tricks users into clicking hidden elements.
<!-- Attacker's site - transparent iframe over fake UI -->
<style>
iframe {
position: absolute;
opacity: 0;
z-index: 100;
}
</style>
<button>Click to win!</button>
<iframe src="https://bank.com/transfer?to=attacker"></iframe>Prevention
# Prevent framing
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
# Or use CSP
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';// JavaScript frame-busting (less reliable)
if (window.self !== window.top) {
window.top.location = window.self.location;
}Content Security Policy (CSP)
CSP restricts which resources can be loaded and executed.
CSP Header
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;CSP Directives
| Directive | Purpose |
|---|---|
default-src | Fallback for other directives |
script-src | JavaScript sources |
style-src | CSS sources |
img-src | Image sources |
font-src | Font sources |
connect-src | AJAX, WebSocket, fetch |
frame-src | iframe sources |
object-src | Plugins (Flash, Java) |
base-uri | Base URL restrictions |
form-action | Form submission targets |
Nonces for Inline Scripts
<!-- Server generates unique nonce per request -->
<script nonce="abc123">
// This inline script is allowed
</script>Content-Security-Policy: script-src 'nonce-abc123'Report-Only Mode
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportSecure Cookies
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=86400
# Secure: Only sent over HTTPS
# HttpOnly: Not accessible via JavaScript
# SameSite=Strict: Not sent with cross-site requests
# SameSite=Lax: Sent with top-level navigations
# SameSite=None: Sent with all requests (requires Secure)Input Validation
// Client-side validation (UX, not security)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError('Invalid email format');
}
// Always validate on server too!
// Use validation libraries
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
age: z.number().int().positive().max(150),
website: z.string().url().optional(),
});
function validateUser(data: unknown) {
return UserSchema.parse(data);
}Secure Communication
HTTPS Enforcement
# HSTS - Force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload<!-- Upgrade HTTP requests to HTTPS -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">Subresource Integrity
<!-- Verify CDN resources haven't been tampered with -->
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>Authentication Security
Token Storage
// Recommended: HttpOnly cookies (set by server)
// Access token in HttpOnly cookie = most secure
// If you must store in JS:
// - sessionStorage for sensitive data (cleared on tab close)
// - Avoid localStorage for tokens (XSS vulnerable)
// Token refresh pattern
async function fetchWithAuth(url: string, options: RequestInit = {}) {
let response = await fetch(url, {
...options,
credentials: 'include', // Send cookies
});
if (response.status === 401) {
// Try to refresh token
const refreshed = await refreshToken();
if (refreshed) {
response = await fetch(url, {
...options,
credentials: 'include',
});
}
}
return response;
}Password Handling
// Never store or log passwords in frontend
// Always use HTTPS for login forms
// Implement rate limiting (server-side)
// Good: Use browser password manager
<input
type="password"
name="password"
autocomplete="current-password"
/>
// New password
<input
type="password"
name="new-password"
autocomplete="new-password"
/>Security Headers
# Comprehensive security headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()Best Practices
Security Guidelines
- Never trust user input - validate and sanitize
- Use Content Security Policy
- Set secure cookie attributes
- Implement CSRF protection
- Use HTTPS everywhere
- Keep dependencies updated
- Use security headers
- Avoid storing sensitive data in localStorage
- Validate on both client and server
- Regular security audits