ure Decision:** Rely on framework auto-escaping where possible, but implement explicit encoding for dynamic contexts. Use a library like DOMPurify only when HTML content is strictly required.
Implementation (TypeScript):
// secure-dom.ts
// Utility for context-aware encoding
export const encodeForHTML = (str: string): string => {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return str.replace(/[&<>"'/]/g, (char) => map[char] || char);
};
export const encodeForAttribute = (str: string): string => {
// Attributes require stricter encoding to prevent attribute breakout
return str.replace(/["&<>'`]/g, (char) => `&#${char.charCodeAt(0)};`);
};
export const encodeForURL = (str: string): string => {
// Use encodeURIComponent for URL parameters, not encodeURI for full URLs
return encodeURIComponent(str);
};
// Safe DOM insertion helper
export const safeSetTextContent = (element: HTMLElement, text: string): void => {
element.textContent = text; // Frameworks should use this under the hood
};
// Example of handling user-generated HTML safely
import DOMPurify from 'dompurify';
export const sanitizeHTML = (html: string): string => {
// Configuration: Allow specific tags, forbid scripts and event handlers
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
FORBID_TAGS: ['script', 'style', 'iframe'],
ADD_ATTR: ['target'],
});
};
2. Content Security Policy (CSP)
CSP is the last line of defense. It instructs the browser to only execute resources from trusted origins. A strict CSP mitigates XSS by preventing inline scripts and unauthorized external resource loading.
Architecture Decision: Implement CSP via HTTP headers rather than meta tags for better performance and reliability. Use nonces for necessary inline scripts.
Implementation:
// csp-config.ts
// Example configuration for a Vite/Webpack build or server middleware
export const generateCSP = (): string => {
const nonce = generateNonce(); // Must be generated per request
return [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' https://trusted-analytics.com`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
`img-src 'self' data: https://cdn.example.com`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`, // Prevents clickjacking
`form-action 'self'`,
`base-uri 'self'`,
`object-src 'none'`,
].join('; ');
};
// Helper to generate cryptographically secure nonce
const generateNonce = (): string => {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array));
};
3. CSRF Mitigation
Cross-Site Request Forgery exploits the trust a site has in a user's browser. For SPAs, the primary defense is using SameSite cookie attributes and custom request headers.
Architecture Decision:
- Cookies: Set
SameSite=Strict or Lax. Avoid None unless absolutely necessary for cross-site integrations, and even then, require Secure and explicit user consent.
- Tokens: For state-changing requests, use the Double Submit Cookie pattern or Custom Header approach. Custom headers are preferred for SPAs using Bearer tokens, as they are not automatically attached by the browser.
Implementation (Axios Interceptor):
// csrf-interceptor.ts
import axios from 'axios';
// Strategy: Custom Header + Token validation
// This assumes the backend sets a CSRF token in a cookie or returns it in an auth response.
const getCsrfToken = (): string | null => {
// In a real scenario, read from a secure cookie or state store
const match = document.cookie.match(/(?:^|;)\s*csrf_token=([^;]+)/);
return match ? decodeURIComponent(match[1]) : null;
};
axios.interceptors.request.use((config) => {
// Only attach CSRF token for state-changing methods
const methodsWithSideEffects = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (methodsWithSideEffects.includes(config.method?.toUpperCase() || '')) {
const token = getCsrfToken();
if (token) {
config.headers['X-CSRF-Token'] = token;
}
}
// Ensure credentials are sent only for same-origin requests
config.withCredentials = true;
return config;
}, (error) => {
return Promise.reject(error);
});
4. DOM-Based XSS Prevention
DOM-based XSS occurs when client-side scripts process untrusted data and write it back to the DOM without encoding.
Best Practice: Treat location.search, location.hash, and document.referrer as untrusted inputs. Never pass these values to sink functions like innerHTML, document.write, or eval.
// safe-url-handling.ts
export const getQueryParam = (key: string): string => {
const params = new URLSearchParams(window.location.search);
const value = params.get(key);
return value ? encodeForHTML(value) : ''; // Encode immediately upon retrieval
};
// Anti-pattern to avoid:
// document.getElementById('output').innerHTML = new URLSearchParams(window.location.search).get('id');
// Safe pattern:
// const id = getQueryParam('id');
// document.getElementById('output').textContent = id;
Pitfall Guide
1. Blind Trust in Framework Auto-Escaping
Mistake: Assuming React/Vue/Angular protects against all XSS.
Reality: Frameworks escape content in standard bindings. However, developers often bypass this using dangerouslySetInnerHTML, v-html, or [innerHTML]. If user input flows into these bindings without sanitization, XSS occurs.
Best Practice: Audit all framework-specific escape hatches. Use DOMPurify if HTML is required.
2. Regex-Based Sanitization
Mistake: Using regular expressions to strip <script> tags.
Reality: Regex sanitization is brittle. Attackers use polyglots, encoding tricks, and browser parsing quirks to bypass regex filters.
Best Practice: Use established libraries like DOMPurify or rely on context-aware encoding. Never roll your own sanitizer.
3. Storing Sensitive Data in LocalStorage
Mistake: Storing JWTs or session tokens in localStorage.
Reality: localStorage is accessible to any JavaScript running on the page. If an XSS vulnerability exists, an attacker can exfiltrate tokens immediately.
Best Practice: Store session tokens in HttpOnly, Secure, SameSite cookies. If JWTs must be used client-side, consider sessionStorage (scope limited to tab) or in-memory storage, though cookies remain the gold standard for security.
Mistake: Adding 'unsafe-inline' to script-src to fix broken functionality without investigating the root cause.
Reality: This effectively disables CSP protection against XSS, allowing injected inline scripts to execute.
Best Practice: Use nonces or hashes for inline scripts. Refactor code to move inline logic to external files.
5. Ignoring SameSite Cookie Attributes
Mistake: Leaving cookies without SameSite attribute or setting SameSite=None without Secure.
Reality: Modern browsers default to Lax, but older browsers or specific cross-origin requests may still be vulnerable. SameSite=None without Secure can lead to token leakage over HTTP.
Best Practice: Explicitly set SameSite=Strict for authentication cookies. Use Lax only if cross-site navigation is required.
6. Third-Party Script Vulnerabilities
Mistake: Loading analytics, ads, or widgets from third-party CDNs without integrity checks.
Reality: Compromised third-party scripts can execute arbitrary code in your domain, bypassing all frontend security controls.
Best Practice: Use Subresource Integrity (SRI) hashes for third-party scripts. Implement CSP to restrict script sources. Consider self-hosting critical dependencies.
7. Forgetting DOM Sinks in Routing
Mistake: Using URL fragments in client-side routing to render content without validation.
Reality: Router parameters often flow directly into component state and then into the DOM. If a route parameter contains a payload, it can trigger XSS.
Best Practice: Validate and encode route parameters before they enter the component state. Treat the URL as an untrusted input source.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| SPA with JWT Auth | Custom X-CSRF-Token Header | Stateless JWTs are not automatically sent by browsers; custom headers prevent CSRF without cookie complexity. | Low |
| Legacy Cookie Auth | SameSite=Strict + Double Submit | Defense-in-depth. SameSite blocks most attacks; double-submit handles legacy edge cases. | Medium |
| User Generated HTML | DOMPurify + CSP object-src 'none' | HTML requires sanitization; CSP prevents object/plugin execution if sanitization fails. | High |
| Micro-Frontend Architecture | Strict CSP + SRI + Isolated Scope | Micro-frontends increase third-party risk; strict CSP and SRI limit blast radius of compromised modules. | High |
| Public Facing Form | SameSite=Lax + CSRF Token + Rate Limiting | Lax allows safe navigation; token protects POST; rate limiting mitigates abuse. | Low |
Configuration Template
CSP Header Configuration (Nginx/Server Example):
# nginx.conf
add_header Content-Security-Policy "default-src 'self'; \
script-src 'self' 'nonce-<DYNAMIC_NONCE>' https://trusted.cdn.com; \
style-src 'self' 'nonce-<DYNAMIC_NONCE>'; \
img-src 'self' data: https://images.example.com; \
font-src 'self' https://fonts.gstatic.com; \
connect-src 'self' https://api.example.com; \
frame-ancestors 'none'; \
form-action 'self'; \
base-uri 'self'; \
object-src 'none';" always;
Axios Security Configuration:
// api-client.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL,
withCredentials: true, // Sends cookies for same-origin
xsrfCookieName: 'csrf_token',
xsrfHeaderName: 'X-CSRF-Token',
timeout: 10000,
});
// Request interceptor for additional security headers
apiClient.interceptors.request.use((config) => {
// Prevent caching of sensitive responses
config.headers['Cache-Control'] = 'no-store';
config.headers['Pragma'] = 'no-cache';
return config;
});
export default apiClient;
Quick Start Guide
-
Install Security Dependencies:
npm install dompurify helmet axios
Note: helmet is for backend/server-side CSP generation; dompurify is for client-side sanitization.
-
Audit and Patch Immediate Risks:
Run a grep for innerHTML and eval. Replace innerHTML with textContent or wrap content in DOMPurify.sanitize(). Remove all eval() calls.
-
Deploy CSP in Report-Only Mode:
Add a CSP header to your server configuration with report-uri pointing to a monitoring endpoint. Deploy to staging and analyze reports to identify legitimate resources that need whitelisting.
-
Enforce Cookie Security:
Update your authentication middleware to set cookies with HttpOnly, Secure, and SameSite=Strict. Verify that the frontend reads tokens from memory or secure storage, not localStorage.
-
Verify CSRF Protection:
Ensure your API client sends CSRF tokens for mutations. If using JWTs, confirm that custom headers are implemented and that the backend validates them. Run a quick test by attempting a cross-origin POST request from a test page to verify rejection.