Threat Modeling for Frontend Engineers
Threat Modeling for Frontend Engineers
A Practical Guide to Thinking Like an Attacker Before They Do
Table of Contents
- Introduction
- Why Frontend Engineers Need Threat Modeling
- The Attacker's Perspective
- Threat Modeling Fundamentals
- The STRIDE Framework
- Frontend Attack Surface Mapping
- Cross-Site Scripting (XSS) Deep Dive
- Cross-Site Request Forgery (CSRF)
- Client-Side Injection Attacks
- Authentication and Session Threats
- Third-Party and Supply Chain Risks
- Data Exposure Vulnerabilities
- Client-Side Storage Security
- API Security from the Frontend
- Content Security Policy (CSP)
- Practical Threat Modeling Process
- Threat Modeling Your React/Vue/Angular App
- Security Code Review Checklist
- Tools for Frontend Security
- Building a Security Mindset
Introduction
Threat modeling is the practice of systematically identifying potential security threats to your application and determining how to address them. For frontend engineers, this means understanding that your code runs in a hostile environment—the user's browser—where attackers have complete control.
┌─────────────────────────────────────────────────────────────────┐
│ The Frontend Security Reality │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Your JavaScript runs in an environment where: │
│ │
│ • Users can read ALL your code │
│ • Users can modify ANY request │
│ • Users can bypass ANY client-side validation │
│ • Users can inject code via console │
│ • Browser extensions can intercept everything │
│ • The network is potentially compromised │
│ • Third-party scripts have full access │
│ │
│ Your job: Build secure applications DESPITE all of this │
│ │
└─────────────────────────────────────────────────────────────────┘
Why Frontend Engineers Need Threat Modeling
The Shifting Security Landscape
┌─────────────────────────────────────────────────────────────────┐
│ Modern Frontend = Expanded Attack Surface │
├─────────────────────────────────────────────────────────────────┤
│ │
│ THEN (2010) NOW (2024) │
│ ───────────────────────────────────────────────────────────── │
│ │
│ • Server renders HTML • Client renders everything │
│ • Minimal JavaScript • 500KB+ JS bundles │
│ • Few dependencies • 1000+ npm packages │
│ • Simple forms • Complex state management │
│ • Server handles auth • JWTs, OAuth in browser │
│ • No client storage • LocalStorage, IndexedDB │
│ • No APIs • GraphQL, REST, WebSockets │
│ • Single origin • Multiple microservices │
│ │
│ Attack surface has grown EXPONENTIALLY │
│ │
└─────────────────────────────────────────────────────────────────┘
Real-World Impact
// These frontend vulnerabilities have caused major breaches:
const realIncidents = {
'British Airways (2018)': {
vulnerability: 'Injected malicious script via Magecart',
impact: '380,000 card details stolen',
fine: '£20 million GDPR fine',
cause: 'Compromised third-party JavaScript',
},
'Ticketmaster (2018)': {
vulnerability: 'Supply chain attack on chat widget',
impact: '40,000 customers affected',
cause: 'Third-party script compromised',
},
'Auth0 (2020)': {
vulnerability: 'XSS in Universal Login page',
impact: 'Account takeover possible',
cause: 'Insufficient input sanitization',
},
'Shopify Stores (Ongoing)': {
vulnerability: 'Insecure third-party apps',
impact: 'Customer data exposure',
cause: 'Overly permissive app permissions',
},
};
The Attacker's Perspective
Think Like an Attacker
Before you can defend, you must understand the offense. Here's how attackers view your frontend:
┌─────────────────────────────────────────────────────────────────┐
│ Attacker's View of Your Frontend │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "Let me see what I have to work with..." │
│ │
│ 1. VIEW SOURCE │
│ └── API endpoints, hidden fields, comments with secrets │
│ │
│ 2. NETWORK TAB │
│ └── API structure, auth tokens, GraphQL schemas │
│ │
│ 3. APPLICATION TAB │
│ └── Cookies, localStorage, IndexedDB data │
│ │
│ 4. CONSOLE │
│ └── Exposed global variables, debug functions │
│ │
│ 5. SOURCE MAPS │
│ └── Original unminified code, variable names │
│ │
│ 6. JS BUNDLE │
│ └── Business logic, validation rules, feature flags │
│ │
│ 7. INPUT FIELDS │
│ └── Every form field is an injection point │
│ │
│ 8. URL PARAMETERS │
│ └── Reflected content, routing manipulation │
│ │
└─────────────────────────────────────────────────────────────────┘
Attacker Motivations
const attackerMotivations = {
// Financial gain
financial: [
'Steal payment card data',
'Cryptocurrency mining (cryptojacking)',
'Ransomware deployment',
'Account takeover for resale',
],
// Data theft
dataTheft: [
'Personal information (PII)',
'Healthcare records',
'Intellectual property',
'Credentials for further attacks',
],
// Disruption
disruption: [
'Defacement',
'Denial of service',
'Reputation damage',
'Competitive sabotage',
],
// Access
access: [
'Pivot to backend systems',
'Lateral movement in organization',
'Supply chain compromise',
'Persistent backdoor',
],
};
Threat Modeling Fundamentals
The Four Questions
Every threat model answers four fundamental questions:
┌─────────────────────────────────────────────────────────────────┐
│ The Four Questions of Threat Modeling │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. WHAT ARE WE BUILDING? │ │
│ │ • System architecture │ │
│ │ • Data flows │ │
│ │ • Trust boundaries │ │
│ │ • Components and dependencies │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2. WHAT CAN GO WRONG? │ │
│ │ • Identify threats using frameworks (STRIDE) │ │
│ │ • Consider attacker capabilities │ │
│ │ • Map attack vectors │ │
│ │ • Review known vulnerability patterns │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 3. WHAT ARE WE GOING TO DO ABOUT IT? │ │
│ │ • Accept the risk │ │
│ │ • Mitigate with controls │ │
│ │ • Transfer (insurance, third-party) │ │
│ │ • Eliminate the feature/component │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 4. DID WE DO A GOOD JOB? │ │
│ │ • Security testing │ │
│ │ • Code review │ │
│ │ • Penetration testing │ │
│ │ • Continuous monitoring │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Data Flow Diagram (DFD)
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Application DFD │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ User │ │
│ │ (Actor) │ │
│ └────┬─────┘ │
│ │ │
│ ═════╪═════════════════════════════════════ TRUST BOUNDARY │
│ │ Browser (User → Browser) │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Browser │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ React │───►│ State │───►│ DOM │ │ │
│ │ │ App │ │ Store │ │ │ │ │
│ │ └─────┬──────┘ └────────────┘ └────────────┘ │ │
│ │ │ │ │
│ │ │ ┌────────────┐ │ │
│ │ │ │ localStorage│ │ │
│ │ │ │ Cookies │ │ │
│ │ │ │ IndexedDB │ │ │
│ │ │ └────────────┘ │ │
│ │ │ │ │
│ └────────┼──────────────────────────────────────────────┘ │
│ │ │
│ ═════════╪═════════════════════════════════ TRUST BOUNDARY │
│ │ Network (Browser → API) │
│ ▼ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Your API │────────►│ Database │ │
│ │ Server │ │ │ │
│ └────────────────┘ └────────────────┘ │
│ │ │
│ ═════════╪═════════════════════════════════ TRUST BOUNDARY │
│ │ (API → Third-party)│
│ ▼ │
│ ┌────────────────┐ │
│ │ Third-Party │ │
│ │ Services │ │
│ └────────────────┘ │
│ │
│ Legend: │
│ ═══════ Trust Boundary (data crosses security domain) │
│ ───────► Data Flow │
│ [ ] Process │
│ │ │ Data Store │
│ │
└─────────────────────────────────────────────────────────────────┘
The STRIDE Framework
STRIDE is a mnemonic for categorizing security threats. Here's how each applies to frontend development:
S - Spoofing (Identity)
// Threat: Attacker pretends to be someone/something they're not
// FRONTEND SPOOFING THREATS:
const spoofingThreats = {
// 1. Session hijacking
sessionHijacking: {
attack: 'Steal session token from localStorage/cookie',
vector: 'XSS, network sniffing, malware',
mitigation: [
'HttpOnly cookies',
'Secure flag',
'Short session expiry',
'Token rotation',
],
},
// 2. Credential theft via phishing
phishing: {
attack: 'Fake login page that looks identical',
vector: 'Lookalike domain, iframe injection',
mitigation: [
'X-Frame-Options: DENY',
'CSP frame-ancestors',
'User education',
],
},
// 3. OAuth token theft
oauthTheft: {
attack: 'Steal authorization code or access token',
vector: 'Open redirect, XSS, referer leakage',
mitigation: [
'PKCE for public clients',
'state parameter validation',
'Short-lived tokens',
],
},
};
// DETECTION: How do you verify identity?
function verifyIdentity() {
// ❌ Bad: Trust client-provided user ID
const userId = localStorage.getItem('userId');
// ✅ Good: Verify with server on every sensitive operation
const response = await fetch('/api/me', {
credentials: 'include', // Sends HttpOnly cookie
});
const user = await response.json();
}
T - Tampering (Data Integrity)
// Threat: Attacker modifies data in transit or at rest
// FRONTEND TAMPERING THREATS:
const tamperingThreats = {
// 1. Parameter manipulation
parameterTampering: {
attack: 'Modify prices, IDs, permissions in requests',
example: `
// User modifies request in DevTools:
POST /api/checkout
{ "productId": "123", "price": 0.01 } // Changed from $99.99
`,
mitigation: [
'Server-side validation',
'NEVER trust client-provided prices',
'Signed tokens for sensitive data',
],
},
// 2. DOM manipulation
domTampering: {
attack: 'Modify hidden fields, disabled buttons',
example: `
// In console:
document.querySelector('[name="isAdmin"]').value = 'true';
document.querySelector('.disabled-btn').disabled = false;
`,
mitigation: [
'Don\'t use hidden fields for security',
'Revalidate everything server-side',
],
},
// 3. LocalStorage manipulation
storageTampering: {
attack: 'Modify stored state, feature flags',
example: `
localStorage.setItem('userRole', 'admin');
localStorage.setItem('premiumFeatures', 'true');
`,
mitigation: [
'Don\'t store authorization decisions client-side',
'Verify feature access server-side',
],
},
};
// EXAMPLE: Price tampering vulnerability
// ❌ VULNERABLE: Trusting client-provided price
async function checkout(cart) {
const total = cart.items.reduce((sum, item) => sum + item.price, 0);
await fetch('/api/charge', {
method: 'POST',
body: JSON.stringify({ amount: total }), // Attacker can modify
});
}
// ✅ SECURE: Server calculates price
async function checkout(cart) {
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
itemIds: cart.items.map(i => i.id), // Only send IDs
}),
});
// Server looks up prices and calculates total
}
R - Repudiation (Non-repudiation)
// Threat: User denies performing an action, no proof exists
// FRONTEND REPUDIATION CONCERNS:
const repudiationThreats = {
// 1. No audit trail
noAuditTrail: {
scenario: 'User claims they never placed order',
problem: 'No server-side logging of user actions',
mitigation: [
'Log all sensitive operations server-side',
'Include timestamps, IP, user agent',
'Capture consent and confirmations',
],
},
// 2. Insufficient logging
insufficientLogging: {
scenario: 'Admin claims they didn\'t delete records',
problem: 'Client-side only logging (easily cleared)',
mitigation: [
'Send audit events to server immediately',
'Log before and after states',
'Immutable audit logs',
],
},
};
// IMPLEMENTATION: Client-side audit helper
class AuditLogger {
async logAction(action, details) {
await fetch('/api/audit', {
method: 'POST',
body: JSON.stringify({
action,
details,
timestamp: new Date().toISOString(),
sessionId: getSessionId(),
// Server adds IP, user agent, user ID
}),
keepalive: true, // Ensure delivery even on page close
});
}
}
// Usage
const audit = new AuditLogger();
await audit.logAction('PURCHASE_INITIATED', { orderId, amount });
I - Information Disclosure
// Threat: Sensitive data exposed to unauthorized parties
// FRONTEND INFORMATION DISCLOSURE THREATS:
const infoDisclosureThreats = {
// 1. Source code exposure
sourceCodeExposure: {
examples: [
'Source maps in production',
'Comments with internal notes',
'Hardcoded API keys',
'Debug code left in',
],
mitigation: [
'Remove source maps in production',
'Use environment variables',
'Strip comments in build',
'Remove console.log statements',
],
},
// 2. Error message leakage
errorLeakage: {
bad: 'Error: Connection to mysql://admin:password123@db.internal:3306 failed',
good: 'Error: Unable to process request. Please try again.',
mitigation: [
'Generic error messages to users',
'Log detailed errors server-side only',
'Use error boundaries in React',
],
},
// 3. API response over-fetching
overFetching: {
bad: {
response: {
id: 1,
email: 'user@example.com',
passwordHash: '$2b$10$...', // Leaked!
ssn: '123-45-6789', // Leaked!
internalNotes: 'VIP customer',
},
},
good: {
response: {
id: 1,
email: 'user@example.com',
// Only what the UI needs
},
},
},
// 4. Sensitive data in URLs
urlExposure: {
bad: '/reset-password?token=abc123&email=user@example.com',
problem: 'Logged in browser history, referer headers, server logs',
good: '/reset-password (token in POST body)',
},
};
// SECURE DATA HANDLING
// ❌ Bad: Exposing sensitive data
function UserProfile({ user }) {
return (
<div>
<p>Name: {user.name}</p>
<p data-ssn={user.ssn}>SSN: ***-**-{user.ssn.slice(-4)}</p>
{/* SSN in data attribute! Visible in DOM */}
</div>
);
}
// ✅ Good: Only display what's needed
function UserProfile({ user }) {
return (
<div>
<p>Name: {user.name}</p>
<p>SSN: ***-**-{user.ssnLastFour}</p>
{/* Server only sends last 4 digits */}
</div>
);
}
D - Denial of Service
// Threat: Making the application unavailable
// FRONTEND DoS THREATS:
const dosThreats = {
// 1. ReDoS (Regular Expression DoS)
redos: {
vulnerable: /^(a+)+$/, // Catastrophic backtracking
attack: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX',
impact: 'Browser tab freezes',
mitigation: [
'Use safe regex patterns',
'Set timeouts on regex operations',
'Use libraries like safe-regex',
],
},
// 2. Memory exhaustion
memoryExhaustion: {
attack: 'Send huge payloads that get stored in memory',
example: 'WebSocket flooding, large file uploads',
mitigation: [
'Limit input sizes client-side',
'Paginate large data sets',
'Use streaming for large files',
],
},
// 3. CPU exhaustion
cpuExhaustion: {
attack: 'Trigger expensive client-side operations',
example: 'Complex SVG rendering, crypto operations',
mitigation: [
'Use Web Workers for heavy computation',
'Debounce/throttle user inputs',
'Set operation timeouts',
],
},
// 4. localStorage quota attacks
storageAttack: {
attack: 'Fill up storage quota to break app',
impact: 'App can\'t save state, crashes',
mitigation: [
'Catch quota exceeded errors',
'Graceful degradation',
'Clear old data when needed',
],
},
};
// PROTECTION: Safe regex with timeout
function safeRegexTest(pattern, input, timeoutMs = 100) {
return new Promise((resolve, reject) => {
const worker = new Worker(URL.createObjectURL(new Blob([`
self.onmessage = function(e) {
const result = new RegExp(e.data.pattern).test(e.data.input);
self.postMessage(result);
};
`])));
const timeout = setTimeout(() => {
worker.terminate();
reject(new Error('Regex timeout'));
}, timeoutMs);
worker.onmessage = (e) => {
clearTimeout(timeout);
worker.terminate();
resolve(e.data);
};
worker.postMessage({ pattern: pattern.source, input });
});
}
E - Elevation of Privilege
// Threat: Gaining capabilities beyond what's authorized
// FRONTEND PRIVILEGE ESCALATION THREATS:
const privilegeThreats = {
// 1. Client-side authorization bypass
authzBypass: {
vulnerable: `
// Checking permissions client-side only
if (user.role === 'admin') {
showAdminPanel();
}
`,
attack: `
// In console:
user.role = 'admin';
showAdminPanel(); // Access granted!
`,
mitigation: [
'ALL authorization checks on server',
'Client checks are UX only, not security',
],
},
// 2. Insecure Direct Object Reference (IDOR)
idor: {
vulnerable: 'GET /api/documents/123 // User\'s own doc',
attack: 'GET /api/documents/124 // Someone else\'s doc',
problem: 'No ownership verification',
mitigation: [
'Verify ownership server-side',
'Use non-guessable IDs (UUIDs)',
'Scoped queries: WHERE user_id = ?',
],
},
// 3. Function-level access control missing
missingFunctionAuth: {
vulnerable: `
// Anyone can call if they know the endpoint
POST /api/admin/delete-user
`,
mitigation: [
'Check permissions for EVERY endpoint',
'Don\'t rely on UI hiding buttons',
],
},
};
// SECURE PATTERN: Defense in depth
// Client-side (UX only)
function AdminPanel({ user }) {
// Hide from non-admins for better UX
if (user.role !== 'admin') {
return null;
}
return <AdminDashboard />;
}
// Server-side (actual security)
app.delete('/api/admin/users/:id', async (req, res) => {
// ALWAYS verify on server
const currentUser = await getUserFromSession(req);
if (currentUser.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
// Check additional constraints
if (currentUser.id === req.params.id) {
return res.status(400).json({ error: 'Cannot delete yourself' });
}
await deleteUser(req.params.id);
});
Frontend Attack Surface Mapping
Complete Attack Surface
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Attack Surface Map │
├─────────────────────────────────────────────────────────────────┤
│ │
│ INPUT VECTORS (Ways data enters the app) │
│ ───────────────────────────────────────── │
│ • URL parameters (?search=<script>) │
│ • URL path (/user/<script>) │
│ • URL hash (#<script>) │
│ • Form inputs (text, hidden, file) │
│ • HTTP headers (rare, but possible) │
│ • Cookies (if read by JS) │
│ • postMessage (cross-origin communication) │
│ • WebSocket messages │
│ • File uploads (names, contents) │
│ • Drag and drop data │
│ • Clipboard paste │
│ • Web Storage (localStorage/sessionStorage) │
│ • IndexedDB │
│ • Third-party widget callbacks │
│ │
│ OUTPUT VECTORS (Where data is rendered/used) │
│ ───────────────────────────────────────────── │
│ • innerHTML / dangerouslySetInnerHTML │
│ • document.write() │
│ • eval() / Function() │
│ • setTimeout/setInterval with strings │
│ • Script src attributes │
│ • Event handler attributes (onclick, onerror) │
│ • CSS (url(), expression()) │
│ • SVG (foreignObject, use href) │
│ • iframe src │
│ • window.location │
│ • anchor href (javascript:) │
│ • form action │
│ │
│ TRUST BOUNDARIES │
│ ───────────────────────────────────────────── │
│ • User input → Application │
│ • Application → DOM │
│ • Application → API │
│ • Third-party scripts → Application │
│ • Different origins (CORS) │
│ • iframes (same-origin policy) │
│ │
└─────────────────────────────────────────────────────────────────┘
Mapping Your Application
// Template for attack surface documentation
const attackSurfaceMap = {
application: 'E-commerce Frontend',
inputVectors: [
{
vector: 'Search bar',
location: '/search?q=',
dataType: 'User text',
rendered: 'Search results page, URL',
threats: ['Reflected XSS', 'URL injection'],
mitigations: ['Output encoding', 'CSP'],
},
{
vector: 'Product reviews',
location: '/products/:id/reviews',
dataType: 'User HTML (rich text)',
rendered: 'Product page, email notifications',
threats: ['Stored XSS', 'Content injection'],
mitigations: ['HTML sanitization', 'CSP', 'sandbox iframes'],
},
{
vector: 'File upload (avatar)',
location: '/settings/avatar',
dataType: 'Image file',
rendered: 'Profile, comments, anywhere avatar shown',
threats: ['XSS via SVG', 'EXIF data leakage', 'Malware'],
mitigations: ['File type validation', 'Image reprocessing', 'CDN serving'],
},
],
thirdPartyScripts: [
{
name: 'Google Analytics',
purpose: 'Analytics',
trust: 'High (Google)',
access: 'Page content, user behavior',
risk: 'Privacy, potential for compromise',
},
{
name: 'Intercom',
purpose: 'Customer chat',
trust: 'Medium',
access: 'DOM, user identity',
risk: 'XSS vector if compromised',
},
],
sensitiveData: [
{
data: 'Session token',
storage: 'HttpOnly cookie',
exposure: 'API requests',
threats: ['Session hijacking'],
},
{
data: 'User email',
storage: 'React state',
exposure: 'Profile page, API responses',
threats: ['Information disclosure'],
},
],
};
Cross-Site Scripting (XSS) Deep Dive
XSS Types
┌─────────────────────────────────────────────────────────────────┐
│ XSS Attack Types │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. REFLECTED XSS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Attacker sends malicious URL to victim │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Victim clicks: example.com/search?q=<script>... │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Server reflects input in response without encoding │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Malicious script executes in victim's browser │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. STORED XSS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Attacker submits malicious content (comment, post) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Server stores the malicious content in database │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Any user viewing that content triggers the script │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Script runs in context of viewing user's session │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. DOM-BASED XSS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Malicious payload in URL fragment or client data │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Client-side JS reads the data without sanitization │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Data written to DOM via innerHTML, eval, etc. │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Script executes (server never sees the payload) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
XSS Payloads and Contexts
// Different contexts require different encoding/escaping
// ═══════════════════════════════════════════════════════════════
// 1. HTML CONTEXT
// ═══════════════════════════════════════════════════════════════
// Vulnerable
element.innerHTML = userInput;
// Attack payloads
const htmlPayloads = [
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'<svg onload=alert(1)>',
'<body onload=alert(1)>',
'<iframe src="javascript:alert(1)">',
'<math><mtext><table><mglyph><style><img src=x onerror=alert(1)>',
];
// Mitigation: Use textContent or encode
element.textContent = userInput; // Safe
element.innerHTML = escapeHtml(userInput); // If HTML needed
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ═══════════════════════════════════════════════════════════════
// 2. ATTRIBUTE CONTEXT
// ═══════════════════════════════════════════════════════════════
// Vulnerable
element.setAttribute('onclick', userInput);
`<a href="${userInput}">`;
// Attack payloads
const attrPayloads = [
'" onclick="alert(1)" data-x="',
"javascript:alert(1)",
"data:text/html,<script>alert(1)</script>",
];
// Mitigation: Validate and encode
// For href, validate URL scheme
function sanitizeUrl(url) {
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '#'; // Block javascript:, data:, etc.
}
return url;
} catch {
return '#';
}
}
// ═══════════════════════════════════════════════════════════════
// 3. JAVASCRIPT CONTEXT
// ═══════════════════════════════════════════════════════════════
// Vulnerable
eval(userInput);
new Function(userInput)();
setTimeout(userInput, 1000);
`<script>var data = ${userInput};</script>`;
// Attack payloads
const jsPayloads = [
'"; alert(1); //',
"'; alert(1); //",
'</script><script>alert(1)</script>',
];
// Mitigation: NEVER use eval with user input
// Use JSON.parse for data
const safeData = JSON.parse(userInput);
// ═══════════════════════════════════════════════════════════════
// 4. CSS CONTEXT
// ═══════════════════════════════════════════════════════════════
// Vulnerable
element.style.cssText = userInput;
`<style>${userInput}</style>`;
// Attack payloads (older browsers)
const cssPayloads = [
'background: url("javascript:alert(1)")',
'expression(alert(1))', // IE only
"}</style><script>alert(1)</script>",
];
// Mitigation: Use specific style properties
element.style.color = sanitizeColor(userInput);
React-Specific XSS Prevention
// React automatically escapes in JSX... mostly
// ✅ SAFE: React escapes this
function SafeComponent({ userInput }) {
return <div>{userInput}</div>; // XSS payload rendered as text
}
// ❌ VULNERABLE: dangerouslySetInnerHTML
function VulnerableComponent({ userHtml }) {
return <div dangerouslySetInnerHTML={{ __html: userHtml }} />;
}
// ❌ VULNERABLE: href with javascript:
function VulnerableLink({ userUrl }) {
return <a href={userUrl}>Click me</a>; // javascript:alert(1) works!
}
// ✅ SAFE: Validate URL protocol
function SafeLink({ userUrl }) {
const safeUrl = useMemo(() => {
try {
const url = new URL(userUrl);
if (url.protocol === 'http:' || url.protocol === 'https:') {
return userUrl;
}
} catch {}
return '#';
}, [userUrl]);
return <a href={safeUrl}>Click me</a>;
}
// ❌ VULNERABLE: Server-side rendering with user data
// pages/user/[id].js (Next.js)
export async function getServerSideProps({ params }) {
// If `id` contains XSS, it could be rendered in error message
const user = await getUser(params.id);
return { props: { user } };
}
// ✅ SAFE: Validate and sanitize params
export async function getServerSideProps({ params }) {
const id = params.id.replace(/[^a-zA-Z0-9-]/g, ''); // Strict allowlist
const user = await getUser(id);
return { props: { user } };
}
HTML Sanitization
// When you MUST render user HTML, use a sanitizer
// Using DOMPurify (recommended)
import DOMPurify from 'dompurify';
function SafeHtmlRenderer({ html }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
ALLOW_DATA_ATTR: false,
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// Configure DOMPurify for your needs
const config = {
ALLOWED_TAGS: ['b', 'i', 'u', 'p', 'a', 'ul', 'ol', 'li', 'br'],
ALLOWED_ATTR: ['href', 'class'],
FORBID_TAGS: ['script', 'style', 'iframe', 'form'],
FORBID_ATTR: ['onclick', 'onerror', 'onload'],
ALLOW_DATA_ATTR: false,
USE_PROFILES: { html: true },
SANITIZE_DOM: true,
};
// Add hooks for additional protection
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// Force all links to open in new tab
if (node.tagName === 'A') {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
// Remove any href that isn't http/https
if (node.hasAttribute('href')) {
const href = node.getAttribute('href');
if (!href.startsWith('http://') && !href.startsWith('https://')) {
node.removeAttribute('href');
}
}
});
Cross-Site Request Forgery (CSRF)
How CSRF Works
┌─────────────────────────────────────────────────────────────────┐
│ CSRF Attack Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. User logs into bank.com (session cookie stored) │
│ │
│ 2. User visits evil.com (in another tab) │
│ │
│ 3. evil.com has hidden form: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <form action="https://bank.com/transfer" method="POST"> │
│ │ <input type="hidden" name="to" value="attacker"> │ │
│ │ <input type="hidden" name="amount" value="10000"> │ │
│ │ </form> │ │
│ │ <script>document.forms[0].submit();</script> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. Browser sends POST to bank.com WITH user's cookies │
│ │
│ 5. bank.com sees valid session, processes transfer │
│ │
│ Result: $10,000 transferred to attacker without user consent │
│ │
└─────────────────────────────────────────────────────────────────┘
CSRF Mitigations
// ═══════════════════════════════════════════════════════════════
// 1. CSRF TOKENS (Synchronizer Token Pattern)
// ═══════════════════════════════════════════════════════════════
// Server generates token and embeds in page
// <meta name="csrf-token" content="abc123">
// Client includes token in requests
async function makeRequest(url, data) {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // Custom header
},
body: JSON.stringify(data),
credentials: 'include',
});
}
// ═══════════════════════════════════════════════════════════════
// 2. SAMESITE COOKIES
// ═══════════════════════════════════════════════════════════════
// Server sets cookie with SameSite attribute
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
// SameSite values:
// Strict: Cookie never sent cross-site (may break OAuth)
// Lax: Cookie sent for top-level navigations (default in modern browsers)
// None: Cookie always sent (requires Secure flag)
// ═══════════════════════════════════════════════════════════════
// 3. DOUBLE-SUBMIT COOKIE
// ═══════════════════════════════════════════════════════════════
// Server sets CSRF token in cookie (non-HttpOnly)
// Client reads cookie and sends as header
// Server compares cookie value with header value
function getCSRFToken() {
return document.cookie
.split('; ')
.find(row => row.startsWith('csrf='))
?.split('=')[1];
}
async function secureRequest(url, data) {
const csrfToken = getCSRFToken();
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
credentials: 'include',
});
}
// ═══════════════════════════════════════════════════════════════
// 4. CUSTOM REQUEST HEADERS
// ═══════════════════════════════════════════════════════════════
// Browsers don't allow cross-origin requests with custom headers
// without CORS preflight approval
// Simply requiring a custom header provides CSRF protection
fetch('/api/data', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest', // Simple custom header
},
credentials: 'include',
});
// Server checks for the header
app.post('/api/data', (req, res) => {
if (!req.headers['x-requested-with']) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
// Process request
});
Framework-Specific CSRF Protection
// NEXT.JS with Server Actions
// Server Actions automatically include CSRF protection
// React with custom hook
function useCSRF() {
const [token, setToken] = useState(null);
useEffect(() => {
// Fetch CSRF token on mount
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setToken(data.token));
}, []);
const csrfFetch = useCallback(async (url, options = {}) => {
return fetch(url, {
...options,
headers: {
...options.headers,
'X-CSRF-Token': token,
},
credentials: 'include',
});
}, [token]);
return { csrfFetch, token };
}
// Axios interceptor
import axios from 'axios';
axios.interceptors.request.use(config => {
const token = document.querySelector('meta[name="csrf-token"]')?.content;
if (token) {
config.headers['X-CSRF-Token'] = token;
}
return config;
});
Client-Side Injection Attacks
Prototype Pollution
// Threat: Attacker modifies Object.prototype
// Vulnerable merge function
function vulnerableMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = vulnerableMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attack
const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
vulnerableMerge({}, maliciousPayload);
// Now ALL objects have isAdmin: true
const user = {};
console.log(user.isAdmin); // true!
// ✅ SAFE merge function
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
// Block prototype pollution
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Or use Object.create(null) for prototype-less objects
const safeObject = Object.create(null);
Template Injection
// Vulnerable: User input in template strings with evaluation
// ❌ BAD: Using eval for templates
function renderTemplate(template, data) {
return eval('`' + template + '`'); // Template injection!
}
const userTemplate = '${require("child_process").execSync("rm -rf /")}';
renderTemplate(userTemplate, {}); // RCE!
// ✅ SAFE: Use proper templating with no code execution
function safeRender(template, data) {
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data.hasOwnProperty(key) ? escapeHtml(data[key]) : match;
});
}
URL Injection
// Threat: Attacker controls URL destinations
// ❌ VULNERABLE: Open redirect
function redirectToReturn() {
const returnUrl = new URLSearchParams(location.search).get('return');
window.location = returnUrl; // Attacker: ?return=https://evil.com
}
// ❌ VULNERABLE: SSRF via image src
function UserAvatar({ avatarUrl }) {
return <img src={avatarUrl} />; // Attacker: avatarUrl = "https://internal-api/admin"
}
// ✅ SAFE: Validate URLs
function safeRedirect(returnUrl) {
try {
const url = new URL(returnUrl, window.location.origin);
// Only allow same-origin redirects
if (url.origin !== window.location.origin) {
console.warn('Blocked cross-origin redirect');
return window.location.href = '/';
}
window.location.href = url.href;
} catch {
window.location.href = '/';
}
}
// ✅ SAFE: Allowlist for external URLs
const ALLOWED_DOMAINS = ['trusted-cdn.com', 'images.example.com'];
function SafeImage({ url }) {
const isAllowed = useMemo(() => {
try {
const parsed = new URL(url);
return ALLOWED_DOMAINS.includes(parsed.hostname);
} catch {
return false;
}
}, [url]);
if (!isAllowed) {
return <img src="/placeholder.png" alt="Invalid image" />;
}
return <img src={url} alt="" />;
}
Authentication and Session Threats
Session Security
// ═══════════════════════════════════════════════════════════════
// TOKEN STORAGE DECISIONS
// ═══════════════════════════════════════════════════════════════
const storageOptions = {
httpOnlyCookie: {
storage: 'Server-set cookie with HttpOnly flag',
xssRisk: 'Protected (JS cannot access)',
csrfRisk: 'Vulnerable (auto-sent with requests)',
mitigation: 'SameSite cookies, CSRF tokens',
recommendation: 'BEST for session tokens',
},
localStorage: {
storage: 'localStorage.setItem("token", jwt)',
xssRisk: 'VULNERABLE (any JS can read)',
csrfRisk: 'Protected (not auto-sent)',
mitigation: 'None for XSS',
recommendation: 'AVOID for sensitive tokens',
},
sessionStorage: {
storage: 'sessionStorage.setItem("token", jwt)',
xssRisk: 'VULNERABLE (any JS can read)',
csrfRisk: 'Protected (not auto-sent)',
mitigation: 'None for XSS',
recommendation: 'AVOID for sensitive tokens',
},
memory: {
storage: 'JavaScript variable in memory',
xssRisk: 'Partially protected (harder to access)',
csrfRisk: 'Protected (not auto-sent)',
mitigation: 'Closure, no global exposure',
recommendation: 'Good for short-lived tokens',
},
};
// ═══════════════════════════════════════════════════════════════
// SECURE TOKEN HANDLING
// ═══════════════════════════════════════════════════════════════
// Pattern: HttpOnly cookie for refresh token, memory for access token
class SecureTokenManager {
#accessToken = null;
async initialize() {
// Refresh token is in HttpOnly cookie
// Request new access token on app start
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Send HttpOnly cookie
});
if (response.ok) {
const { accessToken } = await response.json();
this.#accessToken = accessToken;
}
}
getAccessToken() {
return this.#accessToken;
}
async refreshAccessToken() {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const { accessToken } = await response.json();
this.#accessToken = accessToken;
return accessToken;
}
throw new Error('Session expired');
}
clear() {
this.#accessToken = null;
// Logout endpoint clears HttpOnly cookie
fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
}
}
OAuth/OIDC Security
// ═══════════════════════════════════════════════════════════════
// OAUTH THREATS AND MITIGATIONS
// ═══════════════════════════════════════════════════════════════
const oauthThreats = {
// 1. Authorization code interception
codeInterception: {
threat: 'Attacker intercepts authorization code',
mitigation: 'Use PKCE (Proof Key for Code Exchange)',
},
// 2. State parameter manipulation
csrfViaOAuth: {
threat: 'Attacker initiates OAuth flow for victim',
mitigation: 'Cryptographically random state parameter',
},
// 3. Open redirect in callback
openRedirect: {
threat: 'Attacker redirects user after auth',
mitigation: 'Validate redirect_uri strictly',
},
// 4. Token leakage via referrer
referrerLeakage: {
threat: 'Token in URL leaks via Referer header',
mitigation: 'Use response_mode=fragment or form_post',
},
};
// PKCE Implementation (required for public clients)
class OAuthClient {
async startAuthFlow() {
// Generate PKCE verifier and challenge
const verifier = this.generateCodeVerifier();
const challenge = await this.generateCodeChallenge(verifier);
// Store verifier for later use
sessionStorage.setItem('pkce_verifier', verifier);
// Generate state for CSRF protection
const state = this.generateState();
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
}
async handleCallback() {
const params = new URLSearchParams(window.location.search);
// Verify state parameter
const state = params.get('state');
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
throw new Error('State mismatch - possible CSRF attack');
}
const code = params.get('code');
const verifier = sessionStorage.getItem('pkce_verifier');
// Clean up
sessionStorage.removeItem('oauth_state');
sessionStorage.removeItem('pkce_verifier');
// Exchange code for tokens
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
redirect_uri: REDIRECT_URI,
code_verifier: verifier, // PKCE proof
}),
});
return response.json();
}
generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
async generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
generateState() {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
Third-Party and Supply Chain Risks
The Supply Chain Threat
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Supply Chain Attack Surface │
├─────────────────────────────────────────────────────────────────┤
│ │
│ YOUR APP │
│ │ │
│ ├── package.json (direct dependencies) │
│ │ │ │
│ │ ├── react (trusted) │
│ │ ├── lodash (trusted) │
│ │ ├── random-util-pkg ←── Could be malicious! │
│ │ │ │ │
│ │ │ └── nested-dep ←── You never reviewed this │
│ │ │ │ │
│ │ │ └── another-dep ←── 6 levels deep │
│ │ │ │
│ │ └── ... (500+ packages typically) │
│ │ │
│ ├── CDN scripts │
│ │ ├── https://cdn.example.com/analytics.js │
│ │ │ └── Could be compromised/swapped │
│ │ │ │
│ │ └── https://unpkg.com/some-lib@latest ←── NEVER! │
│ │ └── "latest" can change to malicious version │
│ │ │
│ └── Third-party widgets │
│ ├── Chat widget │
│ ├── Payment processor │
│ └── Analytics │
│ │
│ Real attacks: event-stream, ua-parser-js, colors.js, faker.js │
│ │
└─────────────────────────────────────────────────────────────────┘
Mitigating Supply Chain Risks
// ═══════════════════════════════════════════════════════════════
// 1. LOCK DEPENDENCY VERSIONS
// ═══════════════════════════════════════════════════════════════
// package.json - Use exact versions
{
"dependencies": {
"react": "18.2.0", // ✅ Exact version
"lodash": "^4.17.21" // ❌ Allows minor updates
}
}
// Use package-lock.json / yarn.lock
// Run: npm ci (not npm install) in CI
// ═══════════════════════════════════════════════════════════════
// 2. SUBRESOURCE INTEGRITY (SRI)
// ═══════════════════════════════════════════════════════════════
// For CDN-hosted scripts, use integrity attribute
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
// Generate SRI hash
// openssl dgst -sha384 -binary lib.js | openssl base64 -A
// ═══════════════════════════════════════════════════════════════
// 3. THIRD-PARTY SCRIPT ISOLATION
// ═══════════════════════════════════════════════════════════════
// Load analytics in sandboxed iframe
function loadAnalytics() {
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts'; // No access to parent
iframe.src = 'about:blank';
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Inject analytics code into iframe
iframe.contentDocument.write(`
<script src="https://analytics.example.com/tracker.js"></script>
<script>
// Communicate via postMessage only
window.parent.postMessage({ type: 'pageview', path: '${location.pathname}' }, '*');
</script>
`);
}
// ═══════════════════════════════════════════════════════════════
// 4. DEPENDENCY AUDITING
// ═══════════════════════════════════════════════════════════════
// npm audit (built-in)
// npm audit --production
// Socket.dev (recommended - detects supply chain attacks)
// Snyk
// Dependabot
// ═══════════════════════════════════════════════════════════════
// 5. CONTENT SECURITY POLICY FOR THIRD-PARTY
// ═══════════════════════════════════════════════════════════════
// Limit which scripts can run
Content-Security-Policy:
script-src 'self'
https://trusted-cdn.com
https://www.google-analytics.com;
connect-src 'self'
https://api.example.com
https://www.google-analytics.com;
Evaluating Third-Party Dependencies
// Checklist before adding a dependency
const dependencyChecklist = {
// Popularity and maintenance
popularity: [
'Weekly downloads on npm?',
'GitHub stars?',
'Last commit date?',
'Number of open issues vs closed?',
'Responsive maintainers?',
],
// Security
security: [
'Any known vulnerabilities (npm audit)?',
'Security policy (SECURITY.md)?',
'Previous security incidents?',
'Dependencies of this dependency?',
],
// Code quality
quality: [
'TypeScript types available?',
'Test coverage?',
'Code size (bundlephobia.com)?',
'Tree-shakeable?',
],
// Trust
trust: [
'Who are the maintainers?',
'Is the org/company reputable?',
'Have you reviewed the code?',
'Could you maintain a fork if abandoned?',
],
// Alternatives
alternatives: [
'Is this functionality in the platform? (Intl, fetch, etc.)',
'Could you write this yourself easily?',
'Are there more established alternatives?',
],
};
// Red flags
const redFlags = [
'Very few weekly downloads',
'No repository link',
'Very recent package with lots of dependencies',
'Name similar to popular package (typosquatting)',
'Postinstall scripts',
'Minified source in repo',
'Single maintainer who is unknown',
];
Data Exposure Vulnerabilities
Sensitive Data in Frontend
// ═══════════════════════════════════════════════════════════════
// COMMON DATA EXPOSURE ISSUES
// ═══════════════════════════════════════════════════════════════
// ❌ BAD: Secrets in frontend code
const API_KEY = 'sk_live_abc123'; // Visible to anyone
// ❌ BAD: Secrets in environment variables (client-side)
const API_KEY = process.env.REACT_APP_SECRET_KEY; // Still in bundle!
// ❌ BAD: Debug data in production
console.log('User data:', userData); // Visible in console
// ❌ BAD: Source maps in production
// yoursite.com/static/js/main.js.map exposes source code
// ❌ BAD: Over-fetching sensitive data
const user = await api.getUser(id); // Returns password hash, SSN, etc.
// ❌ BAD: Sensitive data in error messages
catch (error) {
alert(`Database error: ${error.message}`); // Exposes internals
}
// ═══════════════════════════════════════════════════════════════
// SECURE PATTERNS
// ═══════════════════════════════════════════════════════════════
// ✅ GOOD: Backend proxy for API calls with secrets
// Frontend calls your backend, backend adds API key
// ✅ GOOD: Separate bundles for admin vs user
// Don't ship admin code to regular users
// ✅ GOOD: Minimal data fetching
const user = await api.getUser(id, { fields: ['name', 'avatar'] });
// ✅ GOOD: Strip console.* in production
// Use babel-plugin-transform-remove-console
// ✅ GOOD: Don't generate source maps for production
// webpack: devtool: false
// ✅ GOOD: Generic error messages
catch (error) {
console.error(error); // Log for debugging (server-side/monitoring)
showToast('Something went wrong. Please try again.'); // User sees this
}
Preventing Data Leakage
// ═══════════════════════════════════════════════════════════════
// DATA MASKING
// ═══════════════════════════════════════════════════════════════
function maskEmail(email) {
const [local, domain] = email.split('@');
const maskedLocal = local.charAt(0) + '***' + local.charAt(local.length - 1);
return `${maskedLocal}@${domain}`;
}
// john.doe@example.com → j***e@example.com
function maskCard(cardNumber) {
return '****-****-****-' + cardNumber.slice(-4);
}
// 4111111111111111 → ****-****-****-1111
function maskPhone(phone) {
return phone.slice(0, 3) + '-***-' + phone.slice(-4);
}
// 555-123-4567 → 555-***-4567
// ═══════════════════════════════════════════════════════════════
// SECURE LOGGING
// ═══════════════════════════════════════════════════════════════
const sensitiveFields = ['password', 'ssn', 'creditCard', 'token', 'secret'];
function sanitizeForLogging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
if (sensitiveFields.some(field =>
key.toLowerCase().includes(field.toLowerCase())
)) {
return [key, '[REDACTED]'];
}
if (typeof value === 'object') {
return [key, sanitizeForLogging(value)];
}
return [key, value];
})
);
}
// Usage
console.log(sanitizeForLogging({
email: 'user@example.com',
password: 'secret123', // → [REDACTED]
profile: {
name: 'John',
creditCardNumber: '4111...', // → [REDACTED]
},
}));
Client-Side Storage Security
Storage Comparison
┌─────────────────────────────────────────────────────────────────┐
│ Client-Side Storage Security Comparison │
├─────────────────────────────────────────────────────────────────┤
│ │
│ STORAGE XSS CSRF SIZE EXPIRY │
│ ───────────────────────────────────────────────────────────── │
│ │
│ Cookies Depends¹ Yes 4KB Configurable │
│ (HttpOnly=true) Protected Vulnerable │
│ (HttpOnly=false)Vulnerable Vulnerable │
│ │
│ localStorage Vulnerable Protected 5-10MB Never │
│ │
│ sessionStorage Vulnerable Protected 5-10MB Tab close │
│ │
│ IndexedDB Vulnerable Protected Large Never │
│ │
│ ¹ HttpOnly cookies cannot be read by JavaScript │
│ │
│ RECOMMENDATION: │
│ • Session tokens: HttpOnly, Secure, SameSite cookies │
│ • User preferences: localStorage (non-sensitive) │
│ • Cache data: IndexedDB (non-sensitive) │
│ • NEVER store secrets in any client storage JS can access │
│ │
└─────────────────────────────────────────────────────────────────┘
Secure Storage Patterns
// ═══════════════════════════════════════════════════════════════
// ENCRYPTED LOCAL STORAGE (for mildly sensitive data)
// ═══════════════════════════════════════════════════════════════
// Note: This is NOT secure against determined attackers
// XSS can still access the encryption key
// Use for defense-in-depth, not as primary protection
class EncryptedStorage {
#key = null;
async init(passphrase) {
const encoder = new TextEncoder();
const data = encoder.encode(passphrase);
const hash = await crypto.subtle.digest('SHA-256', data);
this.#key = await crypto.subtle.importKey(
'raw',
hash,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
);
}
async setItem(key, value) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(value));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.#key,
data
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
localStorage.setItem(key, btoa(String.fromCharCode(...combined)));
}
async getItem(key) {
const stored = localStorage.getItem(key);
if (!stored) return null;
const combined = Uint8Array.from(atob(stored), c => c.charCodeAt(0));
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
this.#key,
encrypted
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
}
}
// ═══════════════════════════════════════════════════════════════
// SAFE STORAGE WRAPPER WITH EXPIRY
// ═══════════════════════════════════════════════════════════════
class SafeStorage {
setItem(key, value, expiryMinutes = 60) {
const item = {
value,
expiry: Date.now() + expiryMinutes * 60 * 1000,
};
localStorage.setItem(key, JSON.stringify(item));
}
getItem(key) {
const item = localStorage.getItem(key);
if (!item) return null;
try {
const parsed = JSON.parse(item);
if (Date.now() > parsed.expiry) {
localStorage.removeItem(key);
return null;
}
return parsed.value;
} catch {
return null;
}
}
// Clear all expired items
cleanup() {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
this.getItem(key); // Will remove if expired
}
}
}
API Security from the Frontend
Secure API Communication
// ═══════════════════════════════════════════════════════════════
// API CLIENT WITH SECURITY BEST PRACTICES
// ═══════════════════════════════════════════════════════════════
class SecureAPIClient {
#baseURL;
#tokenManager;
constructor(baseURL, tokenManager) {
this.#baseURL = baseURL;
this.#tokenManager = tokenManager;
}
async request(endpoint, options = {}) {
const url = new URL(endpoint, this.#baseURL);
// Get fresh token
const token = await this.#tokenManager.getAccessToken();
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
// CSRF protection for same-origin
'X-Requested-With': 'XMLHttpRequest',
...options.headers,
},
credentials: 'include', // Send cookies for CSRF token
});
// Handle token expiration
if (response.status === 401) {
try {
await this.#tokenManager.refreshAccessToken();
return this.request(endpoint, options); // Retry
} catch {
// Redirect to login
window.location.href = '/login';
throw new Error('Session expired');
}
}
// Don't expose detailed errors
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new APIError(
error.message || 'Request failed',
response.status,
error.code
);
}
return response.json();
}
get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
class APIError extends Error {
constructor(message, status, code) {
super(message);
this.status = status;
this.code = code;
}
}
Input Validation
// ═══════════════════════════════════════════════════════════════
// CLIENT-SIDE VALIDATION (UX, not security!)
// ═══════════════════════════════════════════════════════════════
import { z } from 'zod';
// Define schemas
const userSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[a-z]/, 'Password must contain lowercase letter')
.regex(/[0-9]/, 'Password must contain number'),
name: z.string()
.min(2, 'Name too short')
.max(100, 'Name too long')
.regex(/^[a-zA-Z\s-']+$/, 'Name contains invalid characters'),
});
// Usage in form
function SignupForm() {
const [errors, setErrors] = useState({});
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
// Validate
const result = userSchema.safeParse(data);
if (!result.success) {
const fieldErrors = {};
result.error.issues.forEach(issue => {
fieldErrors[issue.path[0]] = issue.message;
});
setErrors(fieldErrors);
return;
}
// Send to server (which will ALSO validate)
try {
await api.post('/users', result.data);
} catch (error) {
// Handle server-side validation errors
if (error.code === 'VALIDATION_ERROR') {
setErrors(error.details);
}
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields with error display */}
</form>
);
}
// IMPORTANT: Server MUST validate independently
// Client validation is bypassed with:
// fetch('/api/users', { method: 'POST', body: maliciousData })
Content Security Policy (CSP)
Understanding CSP
┌─────────────────────────────────────────────────────────────────┐
│ Content Security Policy Overview │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CSP is a browser security mechanism that controls: │
│ │
│ • What scripts can execute │
│ • What styles can apply │
│ • Where images/fonts/media can load from │
│ • Where forms can submit to │
│ • What URLs can be framed │
│ • What can connect via XHR/WebSocket │
│ │
│ HOW IT WORKS: │
│ │
│ Server sends header: │
│ Content-Security-Policy: script-src 'self' │
│ │
│ Browser enforces: │
│ ✅ <script src="/app.js"> (same origin) │
│ ❌ <script src="https://evil.com"> (blocked) │
│ ❌ <script>alert(1)</script> (inline blocked) │
│ │
│ XSS MITIGATION: │
│ Even if attacker injects <script>alert(1)</script> │
│ CSP prevents it from executing │
│ │
└─────────────────────────────────────────────────────────────────┘
CSP Directives
// ═══════════════════════════════════════════════════════════════
// CSP DIRECTIVE REFERENCE
// ═══════════════════════════════════════════════════════════════
const cspDirectives = {
// Script execution
'script-src': {
purpose: 'Controls JavaScript sources',
values: [
"'self'", // Same origin only
"'unsafe-inline'", // Allow inline scripts (avoid!)
"'unsafe-eval'", // Allow eval() (avoid!)
"'nonce-abc123'", // Allow scripts with this nonce
"'strict-dynamic'", // Trust scripts loaded by trusted scripts
"https://cdn.com", // Specific allowed origin
],
},
// Styles
'style-src': {
purpose: 'Controls CSS sources',
values: ["'self'", "'unsafe-inline'", "'nonce-abc123'"],
},
// Images
'img-src': {
purpose: 'Controls image sources',
values: ["'self'", "data:", "https:", "blob:"],
},
// Fonts
'font-src': {
purpose: 'Controls font sources',
values: ["'self'", "https://fonts.gstatic.com"],
},
// XHR, WebSocket, fetch
'connect-src': {
purpose: 'Controls API/WebSocket connections',
values: ["'self'", "https://api.example.com", "wss://ws.example.com"],
},
// Forms
'form-action': {
purpose: 'Controls form submission destinations',
values: ["'self'"],
},
// Frames
'frame-ancestors': {
purpose: 'Controls who can embed this page',
values: ["'none'", "'self'", "https://trusted.com"],
},
// Base URI
'base-uri': {
purpose: 'Controls <base> element',
values: ["'self'"],
},
// Default
'default-src': {
purpose: 'Fallback for unspecified directives',
values: ["'self'"],
},
};
// ═══════════════════════════════════════════════════════════════
// EXAMPLE CSP POLICIES
// ═══════════════════════════════════════════════════════════════
// STRICT (recommended)
const strictCSP = `
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`;
// With third-party analytics (less strict)
const analyticsCSP = `
default-src 'self';
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
img-src 'self' https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;
`;
// Report-only (for testing)
// Use Content-Security-Policy-Report-Only header
// Violations are reported but not blocked
Implementing CSP
// ═══════════════════════════════════════════════════════════════
// NEXT.JS CSP IMPLEMENTATION
// ═══════════════════════════════════════════════════════════════
// next.config.js
const crypto = require('crypto');
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: generateCSP(),
},
],
},
];
},
};
function generateCSP() {
const nonce = crypto.randomBytes(16).toString('base64');
const policy = {
'default-src': ["'self'"],
'script-src': ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"],
'style-src': ["'self'", `'nonce-${nonce}'`],
'img-src': ["'self'", 'data:', 'https:'],
'font-src': ["'self'"],
'connect-src': ["'self'", 'https://api.example.com'],
'frame-ancestors': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
};
return Object.entries(policy)
.map(([key, values]) => `${key} ${values.join(' ')}`)
.join('; ');
}
// ═══════════════════════════════════════════════════════════════
// USING NONCE WITH REACT
// ═══════════════════════════════════════════════════════════════
// Server generates nonce per request
// pages/_document.js (Next.js)
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document({ nonce }) {
return (
<Html>
<Head nonce={nonce} />
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
// Inline scripts need the nonce
<script nonce={nonce}>
window.__INITIAL_DATA__ = {JSON.stringify(data)};
</script>
// ═══════════════════════════════════════════════════════════════
// CSP VIOLATION REPORTING
// ═══════════════════════════════════════════════════════════════
// Add report-uri or report-to directive
const cspWithReporting = `
default-src 'self';
script-src 'self';
report-uri /api/csp-report;
report-to csp-endpoint;
`;
// Endpoint to receive reports
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
// Log to monitoring service
res.status(204).end();
});
Practical Threat Modeling Process
Step-by-Step Process
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Threat Modeling Workflow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: SCOPE DEFINITION │
│ ───────────────────────── │
│ □ Define feature/component to model │
│ □ Identify stakeholders │
│ □ Set time box (2-4 hours typical) │
│ □ Gather documentation (specs, designs) │
│ │
│ STEP 2: DIAGRAM │
│ ───────────────────────── │
│ □ Draw data flow diagram (DFD) │
│ □ Identify trust boundaries │
│ □ Mark data stores │
│ □ Label all data flows │
│ │
│ STEP 3: IDENTIFY THREATS │
│ ───────────────────────── │
│ □ Walk through STRIDE for each element │
│ □ Consider frontend-specific threats │
│ □ Review OWASP Top 10 │
│ □ Brainstorm attack scenarios │
│ │
│ STEP 4: PRIORITIZE │
│ ───────────────────────── │
│ □ Assess likelihood (1-3) │
│ □ Assess impact (1-3) │
│ □ Calculate risk = likelihood × impact │
│ □ Rank threats by risk score │
│ │
│ STEP 5: MITIGATE │
│ ───────────────────────── │
│ □ Define mitigation for each threat │
│ □ Create user stories/tickets │
│ □ Assign owners │
│ □ Set deadlines │
│ │
│ STEP 6: VALIDATE │
│ ───────────────────────── │
│ □ Security testing │
│ □ Code review │
│ □ Update threat model as system evolves │
│ │
└─────────────────────────────────────────────────────────────────┘
Threat Modeling Template
# Threat Model: [Feature Name]
## Overview
- **Date:** YYYY-MM-DD
- **Participants:** [Names]
- **Scope:** [Description of feature/component]
## Data Flow Diagram
[Insert diagram]
## Assets
| Asset | Sensitivity | Location |
|-------|-------------|----------|
| User credentials | High | Form input, memory |
| Session token | High | HttpOnly cookie |
| User profile | Medium | API response, React state |
## Threats
| ID | Threat | STRIDE | Likelihood | Impact | Risk | Mitigation |
|----|--------|--------|------------|--------|------|------------|
| T1 | XSS via search | Tampering | Medium | High | 6 | Output encoding, CSP |
| T2 | CSRF on profile update | Spoofing | Low | Medium | 2 | SameSite cookies |
| T3 | Token theft via XSS | Info Disclosure | Medium | High | 6 | HttpOnly cookies |
## Mitigations
| ID | Mitigation | Status | Owner | Deadline |
|----|------------|--------|-------|----------|
| M1 | Implement CSP | TODO | @dev | Sprint 5 |
| M2 | Add CSRF tokens | DONE | @dev | Sprint 4 |
## Residual Risks
- [List of accepted risks with justification]
## Review Schedule
- Next review: [Date]
Threat Modeling Your React/Vue/Angular App
Framework-Specific Considerations
// ═══════════════════════════════════════════════════════════════
// REACT THREAT MODEL CHECKLIST
// ═══════════════════════════════════════════════════════════════
const reactSecurityChecklist = {
// XSS Prevention
xss: [
'❌ Using dangerouslySetInnerHTML without sanitization',
'❌ Using href with user input (javascript: URLs)',
'❌ Using eval() or new Function() with user data',
'✅ React auto-escapes JSX content',
'✅ Use DOMPurify for HTML content',
'✅ Validate/sanitize all user inputs',
],
// State Management
state: [
'❌ Storing sensitive data in Redux/context (accessible via DevTools)',
'❌ Persisting tokens to localStorage',
'✅ Sensitive data in HttpOnly cookies',
'✅ Clear sensitive state on logout',
'✅ Use React.memo for sensitive components (prevent unnecessary renders)',
],
// Routing
routing: [
'❌ Open redirects via URL parameters',
'❌ Exposing internal routes',
'✅ Validate redirect destinations',
'✅ Authorization checks in route guards',
'✅ Rate limit route changes (prevent enumeration)',
],
// Props
props: [
'❌ Spreading unknown props (...props) without filtering',
'❌ Trusting props for authorization decisions',
'✅ Define explicit prop types',
'✅ Filter/validate props before use',
],
// Server Components (Next.js 13+)
serverComponents: [
'❌ Exposing server-only data to client components',
'❌ Serializing sensitive data across boundary',
'✅ Use "use server" for sensitive operations',
'✅ Validate data at server boundary',
],
};
// ═══════════════════════════════════════════════════════════════
// COMPONENT SECURITY REVIEW TEMPLATE
// ═══════════════════════════════════════════════════════════════
/*
Component: [Name]
Path: src/components/[Name].jsx
## Data Inputs
- Props: [list props and sources]
- URL params: [list params used]
- API data: [endpoints called]
- User input: [form fields, etc.]
## Security Checklist
□ All user input is validated
□ All output is properly encoded
□ No dangerouslySetInnerHTML with unsanitized data
□ No href with user-controlled URLs (or validated)
□ Authorization checked for sensitive actions
□ Sensitive data not logged
□ Error handling doesn't expose internals
□ Props are validated (PropTypes/TypeScript)
## Threats Identified
1. [Threat description]
- Risk: [High/Medium/Low]
- Mitigation: [Action taken or needed]
## Notes
[Additional security considerations]
*/
Security Code Review Checklist
Pull Request Security Checklist
# Frontend Security Code Review Checklist
## Input Handling
- [ ] All user inputs are validated (client-side for UX, server-side for security)
- [ ] Input length limits are enforced
- [ ] Special characters are handled appropriately
- [ ] File uploads validate type, size, and content
## Output Encoding
- [ ] No dangerouslySetInnerHTML with unsanitized content
- [ ] URLs are validated before use in href/src
- [ ] JSON data is properly escaped before injection into HTML
- [ ] Error messages don't expose sensitive information
## Authentication & Authorization
- [ ] Sensitive operations require authentication
- [ ] Authorization is checked server-side (not just UI hiding)
- [ ] Session tokens are properly managed
- [ ] Logout clears all sensitive data
## Data Protection
- [ ] Sensitive data not stored in localStorage/sessionStorage
- [ ] Sensitive data not logged to console
- [ ] API responses don't over-fetch sensitive fields
- [ ] Source maps disabled in production
## Third-Party Code
- [ ] New dependencies have been evaluated
- [ ] Dependencies are locked to specific versions
- [ ] No known vulnerabilities (npm audit clean)
- [ ] Third-party scripts use SRI when possible
## API Security
- [ ] CSRF protection in place for state-changing requests
- [ ] Authentication tokens sent securely
- [ ] Error responses don't leak implementation details
- [ ] Rate limiting considered for sensitive endpoints
## Security Headers
- [ ] CSP policy is appropriate and not weakened
- [ ] No security headers removed or weakened
- [ ] CORS policy is restrictive as possible
Tools for Frontend Security
Essential Security Tools
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Security Tooling │
├─────────────────────────────────────────────────────────────────┤
│ │
│ STATIC ANALYSIS │
│ ├── ESLint security plugins │
│ │ └── eslint-plugin-security │
│ │ └── eslint-plugin-no-unsanitized │
│ ├── Semgrep (custom rules for your patterns) │
│ └── SonarQube (comprehensive analysis) │
│ │
│ DEPENDENCY SCANNING │
│ ├── npm audit (built-in) │
│ ├── Snyk (comprehensive, CI integration) │
│ ├── Socket.dev (supply chain focus) │
│ └── Dependabot (GitHub native) │
│ │
│ DYNAMIC ANALYSIS │
│ ├── OWASP ZAP (proxy, scanner) │
│ ├── Burp Suite (professional proxy) │
│ └── Browser DevTools (manual testing) │
│ │
│ BROWSER EXTENSIONS │
│ ├── OWASP Penetration Testing Kit │
│ ├── Wappalyzer (technology detection) │
│ └── CSP Evaluator │
│ │
│ HEADER ANALYSIS │
│ ├── securityheaders.com │
│ ├── Mozilla Observatory │
│ └── CSP Evaluator (csp-evaluator.withgoogle.com) │
│ │
│ SECRETS SCANNING │
│ ├── git-secrets │
│ ├── truffleHog │
│ └── Gitleaks │
│ │
└─────────────────────────────────────────────────────────────────┘
ESLint Security Configuration
// .eslintrc.js
module.exports = {
plugins: ['security', 'no-unsanitized'],
extends: [
'plugin:security/recommended',
],
rules: {
// Prevent dangerous functions
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
// Prevent XSS
'no-unsanitized/method': 'error',
'no-unsanitized/property': 'error',
// Security plugin rules
'security/detect-object-injection': 'warn',
'security/detect-non-literal-regexp': 'warn',
'security/detect-unsafe-regex': 'error',
'security/detect-buffer-noassert': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-no-csrf-before-method-override': 'error',
'security/detect-possible-timing-attacks': 'warn',
// React specific
'react/no-danger': 'warn',
'react/no-danger-with-children': 'error',
// No hardcoded secrets
'no-restricted-syntax': [
'error',
{
selector: 'Literal[value=/^(sk_|pk_|api_key|secret)/i]',
message: 'Possible hardcoded secret detected',
},
],
},
};
Building a Security Mindset
Security Principles for Frontend Developers
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Security Principles │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. NEVER TRUST THE CLIENT │
│ Everything from the browser can be manipulated. │
│ All security decisions MUST be made server-side. │
│ │
│ 2. DEFENSE IN DEPTH │
│ Multiple layers of security. If one fails, others protect. │
│ CSP + input validation + output encoding + authentication │
│ │
│ 3. PRINCIPLE OF LEAST PRIVILEGE │
│ Only request permissions/data you actually need. │
│ Only expose functionality users should access. │
│ │
│ 4. SECURE BY DEFAULT │
│ Security should be the default, not an opt-in. │
│ Use frameworks/libraries that are secure by default. │
│ │
│ 5. FAIL SECURELY │
│ When errors occur, fail in a way that doesn't expose data. │
│ Default deny on authorization failures. │
│ │
│ 6. KEEP IT SIMPLE │
│ Complex code has more bugs, including security bugs. │
│ Simple, readable code is easier to secure and review. │
│ │
│ 7. FIX THE ROOT CAUSE │
│ Don't just patch symptoms. Understand why the │
│ vulnerability exists and prevent similar issues. │
│ │
│ 8. ASSUME BREACH │
│ Design assuming attackers will get in. Minimize damage. │
│ Implement detection and response, not just prevention. │
│ │
└─────────────────────────────────────────────────────────────────┘
Continuous Learning
// Resources for staying current
const securityResources = {
learning: [
'OWASP Top 10 (owasp.org/Top10)',
'PortSwigger Web Security Academy (free)',
'HackTheBox / TryHackMe (practice)',
'Google Gruyere (XSS practice)',
],
news: [
'OWASP Newsletter',
'/r/netsec on Reddit',
'Krebs on Security',
'The Hacker News',
'Snyk Blog',
],
newsletters: [
'tl;dr sec',
'This Week in Security',
'JavaScript Weekly (security sections)',
],
practice: [
'OWASP WebGoat',
'Damn Vulnerable Web Application (DVWA)',
'Juice Shop (OWASP)',
'Google CTF challenges',
],
certifications: [
'OSCP (Offensive Security)',
'CEH (EC-Council)',
'Security+ (CompTIA)',
],
};
Summary
┌─────────────────────────────────────────────────────────────────┐
│ Frontend Threat Modeling Summary │
├─────────────────────────────────────────────────────────────────┤
│ │
│ KEY THREATS │
│ • XSS (reflected, stored, DOM-based) │
│ • CSRF │
│ • Supply chain attacks │
│ • Client-side data exposure │
│ • Insecure authentication/session management │
│ • Privilege escalation via client-side checks │
│ │
│ KEY MITIGATIONS │
│ • Content Security Policy (CSP) │
│ • Output encoding / HTML sanitization │
│ • HttpOnly, Secure, SameSite cookies │
│ • CSRF tokens │
│ • Input validation (server-side!) │
│ • Subresource Integrity (SRI) │
│ • Security headers │
│ │
│ PROCESS │
│ 1. Map your attack surface │
│ 2. Apply STRIDE to each component │
│ 3. Prioritize by risk (likelihood × impact) │
│ 4. Implement mitigations │
│ 5. Test and validate │
│ 6. Repeat as system evolves │
│ │
│ GOLDEN RULES │
│ • Never trust client input │
│ • All authorization on the server │
│ • Defense in depth │
│ • Keep dependencies updated │
│ • Security is everyone's responsibility │
│ │
└─────────────────────────────────────────────────────────────────┘
References
- OWASP Top 10
- OWASP Cheat Sheet Series
- MDN Web Security
- Google Web Fundamentals - Security
- Content Security Policy (CSP)
- STRIDE Threat Modeling
Security is not a feature—it's a requirement. Build it in from the start.
What did you think?