Frontend Security Architecture in Modern Applications: Production Deep Dive
Introduction
Frontend security is not a feature—it's an architectural discipline. The naive assumption that "security happens on the backend" has led to catastrophic breaches at scale. The frontend is the attack surface, the trust boundary, and increasingly, the enforcement point for security policies.
Modern frontend applications face a threat landscape that didn't exist a decade ago:
- Supply chain attacks: A single compromised npm package can exfiltrate credentials from millions of users
- XSS at scale: One reflected XSS vulnerability in a CDN-served asset affects every user globally
- Token theft: Browser-based storage is accessible to any JavaScript running in the same origin
- Client-side logic bypass: Business logic enforced only in JavaScript is trivially circumvented
- Third-party script compromise: Analytics, chat widgets, and ad scripts have full DOM access
Production reality at scale (100M+ DAU):
GIF via GIPHY
Attack surface:
- 200+ npm dependencies (direct)
- 1,500+ transitive dependencies
- 15+ third-party scripts (analytics, ads, chat, A/B testing)
- 50+ API endpoints exposed to frontend
- 3+ authentication flows (OAuth, magic link, SSO)
Threat frequency:
- 10,000+ bot requests/minute (credential stuffing)
- 500+ XSS probe attempts/hour
- 50+ dependency vulnerability disclosures/month
- 5+ targeted attacks/week (against high-value accounts)
This article provides a comprehensive architectural framework for frontend security—covering threat modeling, defense-in-depth strategies, implementation patterns, and production incident response.
Threat Model: Understanding the Attack Surface
Browser Security Model Fundamentals
The browser's security model is built on Same-Origin Policy (SOP):
Origin = Protocol + Host + Port
https://app.example.com:443 → Origin A
https://api.example.com:443 → Origin B (different host)
http://app.example.com:80 → Origin C (different protocol)
https://app.example.com:8080 → Origin D (different port)
SOP enforcement:
| Resource Type | Cross-Origin Behavior |
|---|---|
Scripts (<script>) | Loaded and executed (no content access) |
Styles (<link>) | Applied (no content access) |
Images (<img>) | Rendered (no pixel access without CORS) |
Iframes (<iframe>) | Rendered (no DOM access without same-origin) |
| Fetch/XHR | Blocked unless CORS allows |
| Cookies | Sent only to same-origin (unless SameSite=None) |
| localStorage | Strictly same-origin |
Attack Vector Taxonomy
GIF via GIPHY
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND ATTACK VECTORS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ INPUT │ │ SUPPLY │ │ RUNTIME │ │
│ │ ATTACKS │ │ CHAIN │ │ ATTACKS │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ XSS │ │ npm malware │ │ Prototype │ │
│ │ CSRF │ │ Dependency │ │ pollution │ │
│ │ Injection │ │ confusion │ │ DOM clobber │ │
│ │ Open redir │ │ Typosquat │ │ postMessage │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ STORAGE │ │ NETWORK │ │ BROWSER │ │
│ │ ATTACKS │ │ ATTACKS │ │ EXPLOITS │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ Token theft │ │ MITM │ │ Extension │ │
│ │ Session │ │ DNS rebind │ │ malware │ │
│ │ hijacking │ │ WebSocket │ │ Clickjack │ │
│ │ Cookie theft│ │ hijacking │ │ Tabnabbing │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Trust Boundaries in Frontend Architecture
┌──────────────────────────────────────────────────────────────────┐
│ BROWSER │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ YOUR ORIGIN │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Your Code │ │ Third-Party │ │ User │ │ │
│ │ │ (Trusted) │ │ Scripts │ │ Input │ │ │
│ │ │ │ │ (Untrusted) │ │ (Untrusted) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ DOM │ │ │
│ │ │ (Shared, Mutable) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Storage / Cookies │ │ │
│ │ │ (Accessible to ALL scripts) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ NETWORK BOUNDARY │ │
│ │ (HTTPS, but inspectable) │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ BACKEND │
│ (True trust boundary) │
└──────────────────────────────────────────────────────────────────┘
Critical insight: Within your origin, all JavaScript has equal privilege. A compromised third-party script has the same access as your application code—including tokens, cookies, DOM, and network requests.
Defense-in-Depth Architecture
Layer 1: Content Security Policy (CSP)
CSP is the primary defense against XSS and data exfiltration:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /csp-violation;
Production CSP architecture:
// CSP nonce generation (server-side)
const crypto = require('crypto');
function generateCSPNonce() {
return crypto.randomBytes(16).toString('base64');
}
// Middleware
app.use((req, res, next) => {
const nonce = generateCSPNonce();
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
report-uri /api/csp-report;
`.replace(/\s+/g, ' ').trim());
next();
});
// HTML template
<script nonce="<%= cspNonce %>">
// This script executes
</script>
<script>
// This script BLOCKED by CSP
</script>
CSP bypass prevention:
| Bypass Vector | Mitigation |
|---|---|
'unsafe-inline' | Use nonces or hashes exclusively |
| JSONP endpoints | Block with script-src whitelist |
| Angular expression injection | CSP doesn't help—sanitize inputs |
| Base tag injection | base-uri 'self' |
| SVG script injection | object-src 'none' |
Layer 2: Subresource Integrity (SRI)
SRI prevents CDN compromise and MITM attacks:
<script
src="https://cdn.example.com/react.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxAH+..."
crossorigin="anonymous">
</script>
Automated SRI generation pipeline:
// Build-time SRI generation
const crypto = require('crypto');
const fs = require('fs');
function generateSRI(filePath) {
const content = fs.readFileSync(filePath);
const hash = crypto.createHash('sha384').update(content).digest('base64');
return `sha384-${hash}`;
}
// Webpack plugin for automatic SRI
class SRIPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('SRIPlugin', (compilation, callback) => {
const manifest = {};
Object.keys(compilation.assets).forEach(filename => {
if (filename.endsWith('.js') || filename.endsWith('.css')) {
const content = compilation.assets[filename].source();
const hash = crypto.createHash('sha384')
.update(content)
.digest('base64');
manifest[filename] = `sha384-${hash}`;
}
});
compilation.assets['sri-manifest.json'] = {
source: () => JSON.stringify(manifest, null, 2),
size: () => JSON.stringify(manifest).length
};
callback();
});
}
}
GIF via GIPHY
Layer 3: Security Headers
Complete security header configuration:
// Express middleware
const securityHeaders = (req, res, next) => {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// XSS filter (legacy browsers)
res.setHeader('X-XSS-Protection', '1; mode=block');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy (feature policy successor)
res.setHeader('Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=()');
// HSTS (HTTPS enforcement)
res.setHeader('Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload');
// CORP (Cross-Origin Resource Policy)
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
// COEP (Cross-Origin Embedder Policy)
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
// COOP (Cross-Origin Opener Policy)
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
next();
};
Header impact matrix:
| Header | Prevents | Browser Support | Breaking Risk |
|---|---|---|---|
| CSP | XSS, data exfil | 95%+ | High (requires tuning) |
| HSTS | Downgrade attacks | 95%+ | Low |
| X-Frame-Options | Clickjacking | 98%+ | Low |
| SRI | CDN compromise | 90%+ | Medium |
| COOP/COEP | Spectre-class | 85%+ | High (breaks cross-origin) |
Layer 4: Input Sanitization Architecture
Defense layers for user input:
User Input
│
▼
┌─────────────────┐
│ Input Layer │ Validation, type coercion, length limits
└────────┬────────┘
│
▼
┌─────────────────┐
│ Encoding Layer │ Context-aware output encoding
└────────┬────────┘
│
▼
┌─────────────────┐
│ Sanitization │ DOMPurify for HTML, URL validation
└────────┬────────┘
│
▼
┌─────────────────┐
│ CSP Layer │ Block any bypassed XSS
└────────┬────────┘
│
▼
Rendered Output
Production sanitization implementation:
import DOMPurify from 'dompurify';
// Configure DOMPurify
const purifyConfig = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false,
ADD_ATTR: ['target'], // Allow target for links
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
};
// Add hooks for additional protection
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// Force all links to open in new tab with noopener
if (node.tagName === 'A') {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
// Remove javascript: URLs
if (node.hasAttribute('href')) {
const href = node.getAttribute('href');
if (href.toLowerCase().startsWith('javascript:')) {
node.removeAttribute('href');
}
}
});
// Sanitization function
export function sanitizeHTML(dirty: string): string {
return DOMPurify.sanitize(dirty, purifyConfig);
}
// Context-aware encoding
export const encode = {
html: (str: string) => str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''),
attribute: (str: string) => str
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, '''),
javascript: (str: string) => JSON.stringify(str),
url: (str: string) => encodeURIComponent(str),
css: (str: string) => str.replace(/[^\w-]/g, '\\$&'),
};
Authentication Security Architecture
Token Storage Decision Matrix
| Storage | XSS Vulnerable | CSRF Vulnerable | Persistence | Use Case |
|---|---|---|---|---|
| localStorage | ✅ Yes | ❌ No | Permanent | Never for auth tokens |
| sessionStorage | ✅ Yes | ❌ No | Tab only | Short-lived, low-risk |
| Cookie (HttpOnly) | ❌ No | ✅ Yes | Configurable | Primary auth storage |
| Cookie (HttpOnly + SameSite) | ❌ No | ❌ No* | Configurable | Recommended |
| Memory (JS variable) | ✅ Yes | ❌ No | None | Access tokens (short-lived) |
*SameSite=Strict may break legitimate cross-site flows; Lax is often the practical choice.
Secure Token Architecture
Recommended pattern: BFF (Backend-for-Frontend) with HttpOnly cookies:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │────▶│ BFF │────▶│ Auth API │
│ │ │ (Same │ │ │
│ No tokens │ │ Origin) │ │ Issues │
│ in JS │◀────│ │◀────│ Tokens │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
HttpOnly Access Token
Session (never sent
Cookie to browser)
GIF via GIPHY
Implementation:
// BFF authentication endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
// Authenticate with backend
const { accessToken, refreshToken, user } = await authService.login(
email,
password
);
// Store tokens server-side (Redis)
const sessionId = crypto.randomUUID();
await redis.set(`session:${sessionId}`, JSON.stringify({
accessToken,
refreshToken,
userId: user.id,
createdAt: Date.now(),
}), 'EX', 86400); // 24 hour expiry
// Set HttpOnly cookie with session ID only
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 86400000,
path: '/',
domain: '.example.com',
});
// Return non-sensitive user data
res.json({
user: { id: user.id, email: user.email, name: user.name }
});
});
// BFF proxy for API calls
app.use('/api/*', async (req, res) => {
const sessionId = req.cookies.session;
if (!sessionId) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Retrieve tokens from server-side storage
const session = await redis.get(`session:${sessionId}`);
if (!session) {
res.clearCookie('session');
return res.status(401).json({ error: 'Session expired' });
}
const { accessToken } = JSON.parse(session);
// Proxy request to backend with access token
const response = await fetch(`${BACKEND_URL}${req.path}`, {
method: req.method,
headers: {
...req.headers,
'Authorization': `Bearer ${accessToken}`,
'X-Request-ID': req.id,
},
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
});
res.status(response.status).json(await response.json());
});
CSRF Protection
Double-submit cookie pattern with SameSite:
// Generate CSRF token
app.use((req, res, next) => {
if (!req.cookies.csrf) {
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', csrfToken, {
httpOnly: false, // Must be readable by JS
secure: true,
sameSite: 'strict',
maxAge: 86400000,
});
}
next();
});
// Validate CSRF on mutations
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const cookieToken = req.cookies.csrf;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
}
next();
});
// Frontend: Include CSRF token in requests
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf='))
?.split('=')[1];
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
credentials: 'same-origin',
});
Third-Party Script Security
Isolation Strategies
Strategy 1: Sandboxed iframes
<!-- Third-party script in sandboxed iframe -->
<iframe
src="https://widget.third-party.com/embed"
sandbox="allow-scripts allow-same-origin"
allow="encrypted-media"
loading="lazy"
style="border: none; width: 100%; height: 400px;">
</iframe>
Sandbox attribute effects:
| Permission | Effect When Removed |
|---|---|
| allow-scripts | No JavaScript execution |
| allow-same-origin | Treated as unique origin (no storage) |
| allow-forms | Form submission blocked |
| allow-popups | window.open blocked |
| allow-top-navigation | Cannot navigate parent |
Strategy 2: Web Workers for untrusted code
GIF via GIPHY
// Untrusted code execution in Worker (no DOM access)
const workerCode = `
self.onmessage = function(e) {
// Untrusted computation
const result = eval(e.data.code); // Dangerous but isolated
self.postMessage({ result });
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
// Execute untrusted code safely
worker.postMessage({ code: untrustedCode });
worker.onmessage = (e) => {
console.log('Result:', e.data.result);
};
// Timeout protection
setTimeout(() => worker.terminate(), 5000);
Strategy 3: Trusted Types API
// Enable Trusted Types via CSP
// Content-Security-Policy: require-trusted-types-for 'script'
// Create policy for sanitized HTML
const sanitizerPolicy = trustedTypes.createPolicy('sanitizer', {
createHTML: (dirty) => DOMPurify.sanitize(dirty),
createScriptURL: (url) => {
const allowed = ['https://cdn.example.com'];
if (allowed.some(origin => url.startsWith(origin))) {
return url;
}
throw new Error('Blocked script URL: ' + url);
},
});
// Usage (enforced by browser)
element.innerHTML = sanitizerPolicy.createHTML(userInput);
// Direct assignment throws error
element.innerHTML = userInput; // TypeError: requires TrustedHTML
Third-Party Script Monitoring
// Monitor third-party network requests
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const url = new URL(entry.name);
if (url.origin !== location.origin) {
const report = {
url: entry.name,
initiatorType: entry.initiatorType,
duration: entry.duration,
transferSize: entry.transferSize,
origin: url.origin,
};
// Alert on unexpected third-party requests
if (!ALLOWED_ORIGINS.includes(url.origin)) {
console.warn('Unexpected third-party request:', report);
sendSecurityAlert('unauthorized_request', report);
}
}
}
});
observer.observe({ entryTypes: ['resource'] });
// Monitor DOM mutations from third-party scripts
const domObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeName === 'SCRIPT') {
const src = node.src;
if (src && !isAllowedScript(src)) {
node.remove();
sendSecurityAlert('blocked_script_injection', { src });
}
}
}
}
});
domObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
API Security Architecture
Request Signing
Prevent request tampering with HMAC signatures:
// Client-side request signing
async function signedFetch(url: string, options: RequestInit = {}) {
const timestamp = Date.now().toString();
const nonce = crypto.randomUUID();
// Create signature payload
const payload = [
options.method || 'GET',
new URL(url, location.origin).pathname,
timestamp,
nonce,
options.body || '',
].join('\n');
// Sign with client secret (from secure source)
const signature = await hmacSign(payload, CLIENT_SECRET);
return fetch(url, {
...options,
headers: {
...options.headers,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
});
}
// HMAC signing
async function hmacSign(message: string, secret: string): Promise<string> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message)
);
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
Rate Limiting Client-Side
GIF via GIPHY
// Client-side rate limiting (defense in depth, not primary)
class RateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(
private limit: number = 100,
private window: number = 60000
) {}
async acquire(key: string): Promise<boolean> {
const now = Date.now();
const timestamps = this.requests.get(key) || [];
// Remove old timestamps
const valid = timestamps.filter(t => now - t < this.window);
if (valid.length >= this.limit) {
return false;
}
valid.push(now);
this.requests.set(key, valid);
return true;
}
}
// Usage with fetch wrapper
const rateLimiter = new RateLimiter(100, 60000);
async function rateLimitedFetch(url: string, options?: RequestInit) {
const endpoint = new URL(url).pathname;
if (!await rateLimiter.acquire(endpoint)) {
throw new Error('Rate limit exceeded');
}
return fetch(url, options);
}
Sensitive Data Handling
// Redact sensitive data from logs and error reports
const SENSITIVE_FIELDS = [
'password', 'token', 'secret', 'apiKey', 'authorization',
'credit_card', 'ssn', 'cvv', 'pin'
];
function redactSensitive(obj: any, depth = 0): any {
if (depth > 10) return '[MAX_DEPTH]';
if (obj === null || obj === undefined) return obj;
if (typeof obj === 'string') {
// Redact credit card patterns
return obj.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '[REDACTED]');
}
if (Array.isArray(obj)) {
return obj.map(item => redactSensitive(item, depth + 1));
}
if (typeof obj === 'object') {
const redacted: any = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_FIELDS.some(f => key.toLowerCase().includes(f))) {
redacted[key] = '[REDACTED]';
} else {
redacted[key] = redactSensitive(value, depth + 1);
}
}
return redacted;
}
return obj;
}
// Wrap console and error reporting
const originalConsoleLog = console.log;
console.log = (...args) => {
originalConsoleLog(...args.map(arg => redactSensitive(arg)));
};
// Error boundary with redaction
window.addEventListener('error', (event) => {
const sanitizedError = {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
// Don't send full stack traces (may contain sensitive data)
};
sendToErrorService(sanitizedError);
});
Security Monitoring & Incident Response
Security Event Collection
// Security event types
type SecurityEvent = {
type: 'csp_violation' | 'xss_attempt' | 'auth_failure' |
'rate_limit' | 'suspicious_activity';
severity: 'low' | 'medium' | 'high' | 'critical';
details: Record<string, any>;
timestamp: number;
sessionId?: string;
userId?: string;
userAgent: string;
ip?: string;
};
// Security event collector
class SecurityMonitor {
private buffer: SecurityEvent[] = [];
private flushInterval: number;
constructor() {
this.setupCSPReporting();
this.setupXSSDetection();
this.setupAuthMonitoring();
// Flush events every 10 seconds
this.flushInterval = setInterval(() => this.flush(), 10000);
// Flush on page unload
window.addEventListener('beforeunload', () => this.flush());
}
private setupCSPReporting() {
document.addEventListener('securitypolicyviolation', (e) => {
this.record({
type: 'csp_violation',
severity: e.disposition === 'enforce' ? 'high' : 'low',
details: {
blockedURI: e.blockedURI,
violatedDirective: e.violatedDirective,
originalPolicy: e.originalPolicy,
sourceFile: e.sourceFile,
lineNumber: e.lineNumber,
},
});
});
}
private setupXSSDetection() {
// Detect potential XSS in URL
const url = new URL(location.href);
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/on\w+=/i,
/eval\(/i,
];
const searchParams = url.search + url.hash;
for (const pattern of suspiciousPatterns) {
if (pattern.test(searchParams)) {
this.record({
type: 'xss_attempt',
severity: 'high',
details: {
url: location.href,
pattern: pattern.source,
},
});
break;
}
}
}
private setupAuthMonitoring() {
// Monitor for auth anomalies
let failedAttempts = 0;
window.addEventListener('auth:failure', () => {
failedAttempts++;
if (failedAttempts >= 3) {
this.record({
type: 'auth_failure',
severity: 'medium',
details: {
attempts: failedAttempts,
withinMinutes: 5,
},
});
}
});
}
record(event: Omit<SecurityEvent, 'timestamp' | 'userAgent'>) {
this.buffer.push({
...event,
timestamp: Date.now(),
userAgent: navigator.userAgent,
sessionId: getSessionId(),
userId: getUserId(),
});
// Immediate flush for critical events
if (event.severity === 'critical') {
this.flush();
}
}
private async flush() {
if (this.buffer.length === 0) return;
const events = this.buffer.splice(0);
try {
await navigator.sendBeacon(
'/api/security/events',
JSON.stringify(events)
);
} catch (error) {
// Re-add to buffer on failure
this.buffer.unshift(...events);
}
}
}
// Initialize
const securityMonitor = new SecurityMonitor();
Automated Response Actions
GIF via GIPHY
// Security response actions
const securityResponses = {
// Block user session on critical events
blockSession: async (userId: string, reason: string) => {
await fetch('/api/security/block-session', {
method: 'POST',
body: JSON.stringify({ userId, reason }),
});
// Force logout locally
clearAuthState();
window.location.href = '/security-hold';
},
// Increase authentication requirements
requireMFA: async (userId: string) => {
const response = await fetch('/api/auth/require-mfa', {
method: 'POST',
body: JSON.stringify({ userId }),
});
if (response.ok) {
window.location.href = '/mfa-verify';
}
},
// Rate limit aggressive client
enableClientRateLimit: (multiplier: number) => {
localStorage.setItem('rate_limit_multiplier', multiplier.toString());
},
// Disable dangerous features
disableFeature: (feature: string) => {
sessionStorage.setItem(`feature_disabled:${feature}`, 'true');
},
};
// Response decision engine
function handleSecurityEvent(event: SecurityEvent) {
switch (event.type) {
case 'xss_attempt':
if (event.severity === 'critical') {
securityResponses.blockSession(event.userId!, 'XSS attack detected');
}
break;
case 'auth_failure':
if (event.details.attempts >= 5) {
securityResponses.requireMFA(event.userId!);
}
break;
case 'rate_limit':
securityResponses.enableClientRateLimit(2);
break;
}
}
Production Incidents & Lessons
Incident 1: Third-Party Script Exfiltration
Scenario: Analytics vendor script compromised, exfiltrating form data.
Timeline:
- T+0h: Attacker compromises vendor CDN
- T+2h: Modified script begins collecting form inputs
- T+8h: CSP report-uri receives first violation (blocked exfiltration)
- T+8.5h: Security team alerted
- T+9h: Vendor script blocked, incident contained
Impact:
- 0 data exfiltrated (CSP blocked)
- 50,000 users affected (degraded analytics)
Root cause: No SRI on third-party script, relied solely on CSP.
Fix:
<!-- Before: No integrity check -->
<script src="https://vendor.com/analytics.js"></script>
<!-- After: SRI + CSP -->
<script
src="https://vendor.com/analytics.js"
integrity="sha384-abc123..."
crossorigin="anonymous">
</script>
Incident 2: Stored XSS via Rich Text Editor
Scenario: SVG with embedded JavaScript bypassed DOMPurify 2.x.
// Malicious payload that bypassed older DOMPurify
const payload = `<svg><use href="data:image/svg+xml,
<svg id='x' xmlns='http://www.w3.org/2000/svg'>
<image href='x' onerror='alert(1)'/>
</svg>#x">
</use></svg>`;
GIF via GIPHY
Fix:
// Updated DOMPurify config
const config = {
USE_PROFILES: { html: true },
FORBID_TAGS: ['svg', 'math', 'use'],
ADD_TAGS: ['safe-svg'], // Custom element for trusted SVGs
};
// Additional server-side validation
function validateContent(html) {
// Double-check SVG sanitization server-side
if (/<svg|<math|<use/i.test(html)) {
throw new ValidationError('SVG/Math not allowed');
}
return html;
}
Incident 3: JWT Algorithm Confusion
Scenario: Attacker changed JWT algorithm from RS256 to HS256, using public key as HMAC secret.
// Vulnerable verification (accepts any algorithm)
jwt.verify(token, publicKey); // BUG: HS256 with public key succeeds
// Attacker creates token:
const maliciousToken = jwt.sign(
{ sub: 'admin', role: 'admin' },
publicKey, // Public key used as HMAC secret
{ algorithm: 'HS256' }
);
Fix:
// Explicit algorithm whitelist
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Only allow RS256
});
// Better: Use separate keys for different algorithms
const config = {
RS256: { key: rsaPublicKey },
// HS256 not configured = not allowed
};
Security Architecture Checklist
Pre-Launch Security Review
## Authentication & Session Management
- [ ] Tokens stored in HttpOnly cookies (not localStorage)
- [ ] SameSite cookie attribute set (Lax or Strict)
- [ ] Session timeout implemented (idle and absolute)
- [ ] Secure flag on all cookies
- [ ] CSRF protection for state-changing requests
- [ ] JWT algorithm explicitly specified in verification
## Content Security
- [ ] CSP deployed with strict policy
- [ ] CSP report-uri configured and monitored
- [ ] SRI for all third-party scripts
- [ ] X-Frame-Options or frame-ancestors set
- [ ] X-Content-Type-Options: nosniff
- [ ] Trusted Types enabled (if supported)
## Input Validation
- [ ] All user input sanitized before DOM insertion
- [ ] DOMPurify or equivalent for HTML content
- [ ] URL validation for user-provided URLs
- [ ] File upload validation (type, size, content)
## Third-Party Security
- [ ] Third-party script inventory documented
- [ ] SRI hashes for all external scripts
- [ ] CSP restricts script sources
- [ ] Third-party scripts sandboxed where possible
- [ ] Monitoring for unauthorized third-party requests
## API Security
- [ ] CORS configured restrictively
- [ ] Rate limiting implemented
- [ ] Request signing for sensitive operations
- [ ] Sensitive data redacted from logs
- [ ] Error messages don't leak information
## Monitoring & Response
- [ ] Security event logging implemented
- [ ] CSP violations monitored
- [ ] Automated alerts for critical events
- [ ] Incident response playbook documented
- [ ] Regular security audits scheduled
GIF via GIPHY
Future Evolution
Emerging Standards
1. Trusted Types (Stabilizing)
// Enforce DOM XSS sinks require trusted values
// CSP: require-trusted-types-for 'script'
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
createScript: () => { throw new Error('Scripts not allowed'); },
createScriptURL: (url) => validateScriptURL(url),
});
2. Sanitizer API (Proposed)
// Native browser sanitization
const sanitizer = new Sanitizer({
allowElements: ['p', 'b', 'i', 'a'],
allowAttributes: { 'href': ['a'] },
});
element.setHTML(untrustedHTML, { sanitizer });
GIF via GIPHY
3. CHIPS (Cookies Having Independent Partitioned State)
Set-Cookie: session=abc123; Secure; HttpOnly;
SameSite=None; Partitioned
4. Private State Tokens (Trust Tokens)
// Anti-fraud without tracking
const hasToken = await document.hasPrivateToken('https://issuer.example');
if (!hasToken) {
await document.requestPrivateToken('https://issuer.example');
}
Conclusion
Frontend security is architectural, not optional. Production-grade security requires:
- Defense in depth: Multiple overlapping controls (CSP + SRI + sanitization + monitoring)
- Zero trust for third parties: Every external script is a potential attack vector
- Server-side enforcement: Frontend security is defense in depth, not the primary control
- Continuous monitoring: Security events must be collected, analyzed, and actioned
- Incident preparedness: Response playbooks and automated actions for common scenarios
Key metrics to track:
GIF via GIPHY
- CSP violation rate: < 0.01% of page loads
- Third-party script integrity failures: 0
- Authentication anomalies: < 0.1% of sessions
- Time to detect security incidents: < 1 hour
- Time to contain security incidents: < 4 hours
Security is not a feature you ship—it's an architecture you maintain.
What did you think?