Frontend Security: CSP, CORS, and XSS Prevention in Modern Web Apps
In today’s interconnected digital landscape, frontend security has become more critical than ever. With sophisticated attacks targeting client-side applications, implementing robust security measures is not optional—it’s essential. This comprehensive guide explores advanced security techniques for modern web applications, focusing on CSP, CORS, XSS prevention, and enterprise-grade security patterns.
Frontend Security: CSP, CORS, and XSS Prevention in Modern Web Apps
Understanding the Modern Web Security Landscape
The evolution of web applications from simple static pages to complex single-page applications (SPAs) has dramatically expanded the attack surface. Modern frontend applications face numerous security challenges:
- Cross-Site Scripting (XSS): Still the most common vulnerability in web applications
- Cross-Site Request Forgery (CSRF): Exploiting authenticated sessions
- Code Injection: Malicious code execution through various vectors
- Data Exposure: Sensitive information leakage through client-side code
- Third-Party Dependencies: Vulnerabilities in npm packages and CDN resources
- Supply Chain Attacks: Compromised dependencies and build tools
The Security Triad for Frontend Applications
┌─────────────────────────────────────────────────────────────┐
│ Security Layers │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Prevention │ │ Detection │ │ Response │ │
│ │ │ │ │ │ │ │
│ │ • CSP │ │ • Monitoring │ │ • Incident │ │
│ │ • Input │ │ • Logging │ │ Response │ │
│ │ Validation │ │ • Alerts │ │ • Patching │ │
│ │ • CORS │ │ • Auditing │ │ • Recovery │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Content Security Policy (CSP) Implementation
Content Security Policy is a powerful security standard that helps prevent XSS attacks by specifying which content sources are allowed to be loaded and executed.
Advanced CSP Configuration
// src/security/csp.ts
interface CSPDirectives {
[directive: string]: string[];
}
class ContentSecurityPolicyBuilder {
private directives: CSPDirectives = {};
private reportUri?: string;
private reportOnly: boolean = false;
constructor() {
// Set default secure directives
this.setDefaultDirectives();
}
private setDefaultDirectives(): void {
this.directives = {
'default-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'frame-ancestors': ["'none'"],
'object-src': ["'none'"],
};
}
// Directive setters with validation
setScriptSrc(...sources: string[]): this {
this.directives['script-src'] = this.validateSources(sources);
return this;
}
setStyleSrc(...sources: string[]): this {
this.directives['style-src'] = this.validateSources(sources);
return this;
}
setImgSrc(...sources: string[]): this {
this.directives['img-src'] = this.validateSources(sources);
return this;
}
setConnectSrc(...sources: string[]): this {
this.directives['connect-src'] = this.validateSources(sources);
return this;
}
setFontSrc(...sources: string[]): this {
this.directives['font-src'] = this.validateSources(sources);
return this;
}
setMediaSrc(...sources: string[]): this {
this.directives['media-src'] = this.validateSources(sources);
return this;
}
setFrameSrc(...sources: string[]): this {
this.directives['frame-src'] = this.validateSources(sources);
return this;
}
setWorkerSrc(...sources: string[]): this {
this.directives['worker-src'] = this.validateSources(sources);
return this;
}
setChildSrc(...sources: string[]): this {
this.directives['child-src'] = this.validateSources(sources);
return this;
}
setManifestSrc(...sources: string[]): this {
this.directives['manifest-src'] = this.validateSources(sources);
return this;
}
// Special directives
requireSriFor(...types: string[]): this {
this.directives['require-sri-for'] = types;
return this;
}
requireTrustedTypesFor(...types: string[]): this {
this.directives['require-trusted-types-for'] = types;
return this;
}
trustedTypes(...policies: string[]): this {
this.directives['trusted-types'] = policies;
return this;
}
upgradeInsecureRequests(): this {
this.directives['upgrade-insecure-requests'] = [];
return this;
}
blockAllMixedContent(): this {
this.directives['block-all-mixed-content'] = [];
return this;
}
// Reporting configuration
setReportUri(uri: string): this {
this.reportUri = uri;
return this;
}
setReportTo(endpoint: string): this {
this.directives['report-to'] = [endpoint];
return this;
}
enableReportOnly(): this {
this.reportOnly = true;
return this;
}
// Nonce and hash support
addScriptNonce(nonce: string): this {
const scriptSrc = this.directives['script-src'] || [];
scriptSrc.push(`'nonce-${nonce}'`);
this.directives['script-src'] = scriptSrc;
return this;
}
addStyleNonce(nonce: string): this {
const styleSrc = this.directives['style-src'] || [];
styleSrc.push(`'nonce-${nonce}'`);
this.directives['style-src'] = styleSrc;
return this;
}
addScriptHash(algorithm: 'sha256' | 'sha384' | 'sha512', hash: string): this {
const scriptSrc = this.directives['script-src'] || [];
scriptSrc.push(`'${algorithm}-${hash}'`);
this.directives['script-src'] = scriptSrc;
return this;
}
addStyleHash(algorithm: 'sha256' | 'sha384' | 'sha512', hash: string): this {
const styleSrc = this.directives['style-src'] || [];
styleSrc.push(`'${algorithm}-${hash}'`);
this.directives['style-src'] = styleSrc;
return this;
}
// Validation
private validateSources(sources: string[]): string[] {
const validatedSources: string[] = [];
for (const source of sources) {
if (this.isValidSource(source)) {
validatedSources.push(source);
} else {
console.warn(`Invalid CSP source: ${source}`);
}
}
return validatedSources;
}
private isValidSource(source: string): boolean {
// Special keywords
const keywords = ["'self'", "'unsafe-inline'", "'unsafe-eval'", "'none'",
"'strict-dynamic'", "'unsafe-hashes'", "'report-sample'",
"'unsafe-allow-redirects'"];
if (keywords.includes(source)) {
return true;
}
// Nonce or hash
if (source.match(/^'(nonce|sha256|sha384|sha512)-[A-Za-z0-9+/]+=*'$/)) {
return true;
}
// URL scheme
if (source.match(/^(https?|wss?|data|blob):$/)) {
return true;
}
// Host source
if (source.match(/^(\*\.)?[a-z0-9.-]+(:\d+)?$/i)) {
return true;
}
return false;
}
// Build the policy string
build(): string {
const directiveStrings: string[] = [];
for (const [directive, sources] of Object.entries(this.directives)) {
if (sources.length === 0) {
directiveStrings.push(directive);
} else {
directiveStrings.push(`${directive} ${sources.join(' ')}`);
}
}
if (this.reportUri) {
directiveStrings.push(`report-uri ${this.reportUri}`);
}
return directiveStrings.join('; ');
}
// Get header name
getHeaderName(): string {
return this.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
}
}
// Production CSP configuration
export function createProductionCSP(nonce?: string): ContentSecurityPolicyBuilder {
const csp = new ContentSecurityPolicyBuilder()
.setScriptSrc("'self'", "'strict-dynamic'")
.setStyleSrc("'self'", "'unsafe-inline'") // Consider using nonces for styles
.setImgSrc("'self'", 'data:', 'https:')
.setConnectSrc("'self'", 'https://api.example.com', 'wss://ws.example.com')
.setFontSrc("'self'", 'https://fonts.gstatic.com')
.setFrameSrc("'none'")
.setWorkerSrc("'self'")
.upgradeInsecureRequests()
.blockAllMixedContent()
.setReportUri('/api/csp-report')
.setReportTo('csp-endpoint');
if (nonce) {
csp.addScriptNonce(nonce);
}
return csp;
}
// Development CSP configuration (more permissive)
export function createDevelopmentCSP(): ContentSecurityPolicyBuilder {
return new ContentSecurityPolicyBuilder()
.setScriptSrc("'self'", "'unsafe-inline'", "'unsafe-eval'", 'http://localhost:*')
.setStyleSrc("'self'", "'unsafe-inline'")
.setImgSrc("'self'", 'data:', 'http:', 'https:')
.setConnectSrc("'self'", 'http://localhost:*', 'ws://localhost:*')
.setFontSrc("'self'", 'data:')
.setFrameSrc("'self'")
.setWorkerSrc("'self'", 'blob:');
}
Dynamic CSP with Nonce Generation
// src/security/nonceGenerator.ts
import crypto from 'crypto';
export class NonceGenerator {
private static readonly NONCE_LENGTH = 16;
static generate(): string {
return crypto.randomBytes(this.NONCE_LENGTH).toString('base64');
}
static generateForRequest(): { nonce: string; scriptTag: string; styleTag: string } {
const nonce = this.generate();
return {
nonce,
scriptTag: `<script nonce="${nonce}">`,
styleTag: `<style nonce="${nonce}">`,
};
}
}
// Express middleware for CSP with nonce
export function cspMiddleware(req: Request, res: Response, next: NextFunction) {
const nonce = NonceGenerator.generate();
// Store nonce in res.locals for template rendering
res.locals.nonce = nonce;
// Build CSP with nonce
const csp = createProductionCSP(nonce);
// Set CSP header
res.setHeader(csp.getHeaderName(), csp.build());
next();
}
// React component for injecting nonce
export function NonceProvider({ nonce, children }: { nonce: string; children: React.ReactNode }) {
return (
<NonceContext.Provider value={nonce}>
{children}
</NonceContext.Provider>
);
}
// Hook for using nonce in React components
export function useNonce(): string {
const nonce = useContext(NonceContext);
if (!nonce) {
throw new Error('useNonce must be used within NonceProvider');
}
return nonce;
}
CSP Violation Reporting
// src/security/cspReporting.ts
interface CSPViolationReport {
'csp-report': {
'document-uri': string;
'referrer': string;
'violated-directive': string;
'effective-directive': string;
'original-policy': string;
'disposition': string;
'blocked-uri': string;
'line-number'?: number;
'column-number'?: number;
'source-file'?: string;
'status-code': number;
'script-sample'?: string;
};
}
export class CSPReportHandler {
private violations: CSPViolationReport[] = [];
private reportEndpoint: string;
constructor(reportEndpoint: string) {
this.reportEndpoint = reportEndpoint;
}
async handleReport(report: CSPViolationReport): Promise<void> {
// Validate report
if (!this.isValidReport(report)) {
console.warn('Invalid CSP report received');
return;
}
// Store violation
this.violations.push(report);
// Analyze violation
const analysis = this.analyzeViolation(report);
// Log for monitoring
console.error('CSP Violation:', {
directive: report['csp-report']['violated-directive'],
blockedUri: report['csp-report']['blocked-uri'],
documentUri: report['csp-report']['document-uri'],
analysis,
});
// Send to monitoring service
await this.sendToMonitoring(report, analysis);
// Check for patterns
this.checkViolationPatterns();
}
private isValidReport(report: any): report is CSPViolationReport {
return report &&
typeof report === 'object' &&
'csp-report' in report &&
typeof report['csp-report'] === 'object' &&
'violated-directive' in report['csp-report'] &&
'blocked-uri' in report['csp-report'];
}
private analyzeViolation(report: CSPViolationReport): {
severity: 'low' | 'medium' | 'high' | 'critical';
category: string;
recommendation: string;
} {
const violation = report['csp-report'];
// Determine severity based on directive
let severity: 'low' | 'medium' | 'high' | 'critical' = 'low';
let category = 'unknown';
let recommendation = '';
if (violation['violated-directive'].startsWith('script-src')) {
severity = 'critical';
category = 'script-injection';
recommendation = 'Review script sources and update CSP if legitimate';
} else if (violation['violated-directive'].startsWith('style-src')) {
severity = 'medium';
category = 'style-injection';
recommendation = 'Consider using nonces for inline styles';
} else if (violation['violated-directive'].startsWith('img-src')) {
severity = 'low';
category = 'image-loading';
recommendation = 'Verify image source is trusted';
} else if (violation['violated-directive'].startsWith('connect-src')) {
severity = 'high';
category = 'api-connection';
recommendation = 'Audit API endpoints and update CSP if needed';
}
// Check for known attack patterns
if (this.isKnownAttackPattern(violation['blocked-uri'])) {
severity = 'critical';
category = 'attack-attempt';
recommendation = 'Potential attack detected - investigate immediately';
}
return { severity, category, recommendation };
}
private isKnownAttackPattern(blockedUri: string): boolean {
const attackPatterns = [
/javascript:/i,
/data:text\/html/i,
/vbscript:/i,
/file:\/\//i,
/chrome-extension:/i,
];
return attackPatterns.some(pattern => pattern.test(blockedUri));
}
private async sendToMonitoring(
report: CSPViolationReport,
analysis: any
): Promise<void> {
try {
await fetch(this.reportEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
report,
analysis,
timestamp: new Date().toISOString(),
userAgent: report['csp-report']['document-uri'],
}),
});
} catch (error) {
console.error('Failed to send CSP report to monitoring:', error);
}
}
private checkViolationPatterns(): void {
// Check for repeated violations
const recentViolations = this.violations.slice(-100);
const violationCounts = new Map<string, number>();
for (const violation of recentViolations) {
const key = `${violation['csp-report']['violated-directive']}:${violation['csp-report']['blocked-uri']}`;
violationCounts.set(key, (violationCounts.get(key) || 0) + 1);
}
// Alert on repeated violations
for (const [key, count] of violationCounts) {
if (count > 10) {
console.warn(`Repeated CSP violation detected: ${key} (${count} times)`);
// Trigger alert
this.triggerAlert(key, count);
}
}
}
private triggerAlert(violation: string, count: number): void {
// Implement alerting mechanism
console.error(`ALERT: CSP violation threshold exceeded for ${violation}`);
}
}
CORS Configuration Best Practices
Cross-Origin Resource Sharing (CORS) is crucial for controlling access to your resources from different origins while maintaining security.
Advanced CORS Implementation
// src/security/cors.ts
interface CORSOptions {
origins: string[] | ((origin: string) => boolean);
methods?: string[];
allowedHeaders?: string[];
exposedHeaders?: string[];
credentials?: boolean;
maxAge?: number;
preflightContinue?: boolean;
optionsSuccessStatus?: number;
}
export class CORSManager {
private options: CORSOptions;
private allowedOrigins: Set<string> = new Set();
constructor(options: CORSOptions) {
this.options = {
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
credentials: true,
maxAge: 86400, // 24 hours
preflightContinue: false,
optionsSuccessStatus: 204,
...options,
};
if (Array.isArray(options.origins)) {
options.origins.forEach(origin => this.allowedOrigins.add(origin));
}
}
middleware(): (req: Request, res: Response, next: NextFunction) => void {
return (req, res, next) => {
const origin = req.headers.origin;
if (!origin) {
// Same-origin request
next();
return;
}
// Check if origin is allowed
if (this.isOriginAllowed(origin)) {
this.setHeaders(res, origin);
} else {
// Log blocked origin for monitoring
console.warn(`CORS: Blocked request from origin: ${origin}`);
}
// Handle preflight requests
if (req.method === 'OPTIONS') {
if (this.options.preflightContinue) {
next();
} else {
res.sendStatus(this.options.optionsSuccessStatus!);
}
} else {
next();
}
};
}
private isOriginAllowed(origin: string): boolean {
if (typeof this.options.origins === 'function') {
return this.options.origins(origin);
}
// Check exact match
if (this.allowedOrigins.has(origin)) {
return true;
}
// Check wildcard subdomain matching
for (const allowed of this.allowedOrigins) {
if (allowed.startsWith('*.')) {
const domain = allowed.substring(2);
const regex = new RegExp(`^https?://[^.]+\\.${domain.replace('.', '\\.')}$`);
if (regex.test(origin)) {
return true;
}
}
}
return false;
}
private setHeaders(res: Response, origin: string): void {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
if (this.options.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (this.options.methods) {
res.setHeader('Access-Control-Allow-Methods', this.options.methods.join(', '));
}
if (this.options.allowedHeaders) {
res.setHeader('Access-Control-Allow-Headers', this.options.allowedHeaders.join(', '));
}
if (this.options.exposedHeaders) {
res.setHeader('Access-Control-Expose-Headers', this.options.exposedHeaders.join(', '));
}
if (this.options.maxAge) {
res.setHeader('Access-Control-Max-Age', this.options.maxAge.toString());
}
}
// Dynamic origin validation
static createDynamicOriginValidator(config: {
allowedDomains: string[];
allowLocalhost?: boolean;
allowSubdomains?: boolean;
}): (origin: string) => boolean {
return (origin: string) => {
try {
const url = new URL(origin);
// Allow localhost in development
if (config.allowLocalhost && url.hostname === 'localhost') {
return true;
}
// Check allowed domains
for (const domain of config.allowedDomains) {
if (config.allowSubdomains) {
if (url.hostname === domain || url.hostname.endsWith(`.${domain}`)) {
return true;
}
} else {
if (url.hostname === domain) {
return true;
}
}
}
return false;
} catch {
return false;
}
};
}
}
// Production CORS configuration
export const productionCORS = new CORSManager({
origins: CORSManager.createDynamicOriginValidator({
allowedDomains: ['example.com', 'app.example.com'],
allowLocalhost: false,
allowSubdomains: true,
}),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
exposedHeaders: ['X-Total-Count', 'X-RateLimit-Remaining'],
});
// Development CORS configuration
export const developmentCORS = new CORSManager({
origins: CORSManager.createDynamicOriginValidator({
allowedDomains: ['localhost', '127.0.0.1'],
allowLocalhost: true,
allowSubdomains: false,
}),
credentials: true,
});
CORS Security Patterns
// src/security/corsPatterns.ts
export class CORSSecurityPatterns {
// Pattern 1: Whitelist with environment-based configuration
static createEnvironmentBasedCORS(): CORSManager {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
return new CORSManager({
origins: (origin) => {
// Always allow same-origin requests
if (!origin) return true;
// Check whitelist
return allowedOrigins.some(allowed => {
if (allowed === origin) return true;
// Support wildcard subdomains
if (allowed.startsWith('*.')) {
const domain = allowed.substring(2);
const regex = new RegExp(`^https?://[^.]+\\.${domain.replace('.', '\\.')}$`);
return regex.test(origin);
}
return false;
});
},
credentials: true,
});
}
// Pattern 2: Token-based CORS validation
static createTokenBasedCORS(validateToken: (token: string) => Promise<boolean>): CORSManager {
return new CORSManager({
origins: async (origin) => {
// Extract token from request
const token = this.extractTokenFromOrigin(origin);
if (!token) return false;
// Validate token
return await validateToken(token);
},
credentials: false, // Don't use cookies with token-based auth
});
}
// Pattern 3: Time-based CORS restrictions
static createTimeBasedCORS(allowedOrigins: string[], restrictedHours?: {
start: number;
end: number;
}): CORSManager {
return new CORSManager({
origins: (origin) => {
if (!allowedOrigins.includes(origin)) {
return false;
}
if (restrictedHours) {
const hour = new Date().getHours();
if (hour >= restrictedHours.start && hour < restrictedHours.end) {
console.warn(`CORS: Access restricted during hours ${restrictedHours.start}-${restrictedHours.end}`);
return false;
}
}
return true;
},
});
}
// Pattern 4: Rate-limited CORS
static createRateLimitedCORS(
allowedOrigins: string[],
rateLimit: { windowMs: number; max: number }
): CORSManager {
const requestCounts = new Map<string, { count: number; resetTime: number }>();
return new CORSManager({
origins: (origin) => {
if (!allowedOrigins.includes(origin)) {
return false;
}
const now = Date.now();
const record = requestCounts.get(origin) || { count: 0, resetTime: now + rateLimit.windowMs };
if (now > record.resetTime) {
// Reset window
record.count = 0;
record.resetTime = now + rateLimit.windowMs;
}
record.count++;
requestCounts.set(origin, record);
if (record.count > rateLimit.max) {
console.warn(`CORS: Rate limit exceeded for origin ${origin}`);
return false;
}
return true;
},
});
}
private static extractTokenFromOrigin(origin: string): string | null {
// Example: Extract token from subdomain
const match = origin.match(/^https?:\/\/([^.]+)\.api\.example\.com$/);
return match ? match[1] : null;
}
}
XSS Prevention Techniques
Cross-Site Scripting remains one of the most dangerous vulnerabilities. Here’s a comprehensive approach to preventing XSS attacks.
Input Validation and Sanitization
// src/security/xss.ts
import DOMPurify from 'isomorphic-dompurify';
import { z } from 'zod';
export class XSSProtection {
private static readonly DANGEROUS_TAGS = [
'script', 'iframe', 'object', 'embed', 'link', 'style', 'form', 'input',
'button', 'select', 'textarea', 'meta', 'base'
];
private static readonly DANGEROUS_ATTRIBUTES = [
'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout', 'onkeydown',
'onkeyup', 'onchange', 'onfocus', 'onblur', 'onsubmit', 'ondblclick',
'onmouseenter', 'onmouseleave', 'oncontextmenu', 'formaction', 'style'
];
// Sanitize HTML content
static sanitizeHTML(dirty: string, options?: {
allowedTags?: string[];
allowedAttributes?: string[];
allowDataAttributes?: boolean;
}): string {
const config: any = {
ALLOWED_TAGS: options?.allowedTags || ['p', 'br', 'span', 'div', 'a', 'strong', 'em', 'ul', 'ol', 'li'],
ALLOWED_ATTR: options?.allowedAttributes || ['href', 'title', 'class'],
ALLOW_DATA_ATTR: options?.allowDataAttributes || false,
RETURN_TRUSTED_TYPE: false,
};
// Additional security: Remove dangerous protocols
config.ALLOWED_URI_REGEXP = /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
return DOMPurify.sanitize(dirty, config);
}
// Escape HTML entities
static escapeHTML(unsafe: string): string {
const map: { [key: string]: string } = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return unsafe.replace(/[&<>"'/]/g, (char) => map[char]);
}
// Validate and sanitize JSON
static sanitizeJSON(input: any): any {
if (typeof input === 'string') {
try {
// Parse and re-stringify to remove any code
return JSON.parse(JSON.stringify(JSON.parse(input)));
} catch {
throw new Error('Invalid JSON input');
}
}
// Deep clone and sanitize object
return this.deepSanitizeObject(input);
}
private static deepSanitizeObject(obj: any): any {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => this.deepSanitizeObject(item));
}
const sanitized: any = {};
for (const [key, value] of Object.entries(obj)) {
// Skip prototype pollution attempts
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
// Sanitize key
const sanitizedKey = this.escapeHTML(key);
// Recursively sanitize value
if (typeof value === 'string') {
sanitized[sanitizedKey] = this.escapeHTML(value);
} else if (typeof value === 'object') {
sanitized[sanitizedKey] = this.deepSanitizeObject(value);
} else {
sanitized[sanitizedKey] = value;
}
}
return sanitized;
}
// URL validation and sanitization
static sanitizeURL(url: string): string {
try {
const parsed = new URL(url);
// Only allow safe protocols
const safeProtocols = ['http:', 'https:', 'mailto:'];
if (!safeProtocols.includes(parsed.protocol)) {
throw new Error('Unsafe protocol');
}
// Reconstruct URL to remove any injected code
return parsed.toString();
} catch {
// If URL parsing fails, return empty string
return '';
}
}
// Create safe HTML templates
static createSafeTemplate(strings: TemplateStringsArray, ...values: any[]): string {
let result = '';
for (let i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
// Escape all interpolated values
result += this.escapeHTML(String(values[i]));
}
}
return result;
}
// Schema-based validation
static createValidator<T>(schema: z.ZodSchema<T>) {
return (data: unknown): T => {
try {
// Validate against schema
const validated = schema.parse(data);
// Additional sanitization
return this.deepSanitizeObject(validated) as T;
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Validation failed: ${error.errors.map(e => e.message).join(', ')}`);
}
throw error;
}
};
}
}
// React component for safe rendering
export function SafeHTML({
content,
allowedTags,
allowedAttributes,
className
}: {
content: string;
allowedTags?: string[];
allowedAttributes?: string[];
className?: string;
}) {
const sanitized = XSSProtection.sanitizeHTML(content, {
allowedTags,
allowedAttributes,
});
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: sanitized }}
/>
);
}
// Hook for XSS protection
export function useXSSProtection() {
const sanitizeInput = useCallback((input: string) => {
return XSSProtection.escapeHTML(input);
}, []);
const sanitizeHTML = useCallback((html: string, options?: any) => {
return XSSProtection.sanitizeHTML(html, options);
}, []);
const sanitizeURL = useCallback((url: string) => {
return XSSProtection.sanitizeURL(url);
}, []);
return {
sanitizeInput,
sanitizeHTML,
sanitizeURL,
};
}
DOM-based XSS Prevention
// src/security/domXssPrevention.ts
export class DOMXSSPrevention {
// Safe DOM manipulation
static setText(element: HTMLElement, text: string): void {
element.textContent = text; // Always safe
}
static setHTML(element: HTMLElement, html: string): void {
// Use DOMPurify for HTML content
const clean = DOMPurify.sanitize(html);
element.innerHTML = clean;
}
static setAttribute(element: HTMLElement, name: string, value: string): void {
// Validate attribute name
if (this.isDangerousAttribute(name)) {
console.error(`Blocked dangerous attribute: ${name}`);
return;
}
// Special handling for certain attributes
if (name === 'href' || name === 'src') {
const sanitized = XSSProtection.sanitizeURL(value);
if (sanitized) {
element.setAttribute(name, sanitized);
}
} else {
element.setAttribute(name, XSSProtection.escapeHTML(value));
}
}
private static isDangerousAttribute(name: string): boolean {
return name.startsWith('on') || name === 'formaction' || name === 'style';
}
// Safe element creation
static createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
options?: {
text?: string;
html?: string;
attributes?: Record<string, string>;
children?: HTMLElement[];
}
): HTMLElementTagNameMap[K] {
const element = document.createElement(tagName);
if (options?.text) {
this.setText(element, options.text);
} else if (options?.html) {
this.setHTML(element, options.html);
}
if (options?.attributes) {
for (const [name, value] of Object.entries(options.attributes)) {
this.setAttribute(element, name, value);
}
}
if (options?.children) {
for (const child of options.children) {
element.appendChild(child);
}
}
return element;
}
// Safe event handler attachment
static addEventListener(
element: HTMLElement,
event: string,
handler: EventListener,
options?: AddEventListenerOptions
): void {
// Validate event type
const allowedEvents = [
'click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout',
'keydown', 'keyup', 'keypress', 'focus', 'blur', 'change', 'input',
'submit', 'load', 'error', 'resize', 'scroll'
];
if (!allowedEvents.includes(event)) {
console.warn(`Potentially dangerous event type: ${event}`);
}
element.addEventListener(event, handler, options);
}
// Safe URL manipulation
static updateURL(url: string, params?: Record<string, string>): string {
try {
const urlObj = new URL(url, window.location.origin);
if (params) {
for (const [key, value] of Object.entries(params)) {
urlObj.searchParams.set(
XSSProtection.escapeHTML(key),
XSSProtection.escapeHTML(value)
);
}
}
return urlObj.toString();
} catch {
return '';
}
}
}
// React hook for safe DOM operations
export function useSafeDOM() {
const setTextContent = useCallback((ref: React.RefObject<HTMLElement>, text: string) => {
if (ref.current) {
DOMXSSPrevention.setText(ref.current, text);
}
}, []);
const setHTMLContent = useCallback((ref: React.RefObject<HTMLElement>, html: string) => {
if (ref.current) {
DOMXSSPrevention.setHTML(ref.current, html);
}
}, []);
const setSafeAttribute = useCallback((
ref: React.RefObject<HTMLElement>,
name: string,
value: string
) => {
if (ref.current) {
DOMXSSPrevention.setAttribute(ref.current, name, value);
}
}, []);
return {
setTextContent,
setHTMLContent,
setSafeAttribute,
};
}
Authentication and Authorization Patterns
Implementing secure authentication and authorization is crucial for protecting your application.
JWT-based Authentication with Security Best Practices
// src/auth/jwtAuth.ts
import jwt from 'jsonwebtoken';
import { z } from 'zod';
const TokenPayloadSchema = z.object({
sub: z.string(), // Subject (user ID)
email: z.string().email(),
roles: z.array(z.string()),
permissions: z.array(z.string()),
iat: z.number(),
exp: z.number(),
jti: z.string(), // JWT ID for revocation
});
type TokenPayload = z.infer<typeof TokenPayloadSchema>;
export class JWTAuthManager {
private readonly accessTokenSecret: string;
private readonly refreshTokenSecret: string;
private readonly accessTokenExpiry: string;
private readonly refreshTokenExpiry: string;
private readonly issuer: string;
private readonly audience: string;
private revokedTokens: Set<string> = new Set();
constructor(config: {
accessTokenSecret: string;
refreshTokenSecret: string;
accessTokenExpiry?: string;
refreshTokenExpiry?: string;
issuer: string;
audience: string;
}) {
this.accessTokenSecret = config.accessTokenSecret;
this.refreshTokenSecret = config.refreshTokenSecret;
this.accessTokenExpiry = config.accessTokenExpiry || '15m';
this.refreshTokenExpiry = config.refreshTokenExpiry || '7d';
this.issuer = config.issuer;
this.audience = config.audience;
}
generateTokenPair(user: {
id: string;
email: string;
roles: string[];
permissions: string[];
}): {
accessToken: string;
refreshToken: string;
expiresIn: number;
} {
const jti = this.generateJTI();
const now = Math.floor(Date.now() / 1000);
const payload: TokenPayload = {
sub: user.id,
email: user.email,
roles: user.roles,
permissions: user.permissions,
iat: now,
exp: now + this.parseExpiry(this.accessTokenExpiry),
jti,
};
const accessToken = jwt.sign(payload, this.accessTokenSecret, {
algorithm: 'HS256',
issuer: this.issuer,
audience: this.audience,
});
const refreshPayload = {
sub: user.id,
jti: this.generateJTI(),
iat: now,
exp: now + this.parseExpiry(this.refreshTokenExpiry),
};
const refreshToken = jwt.sign(refreshPayload, this.refreshTokenSecret, {
algorithm: 'HS256',
issuer: this.issuer,
audience: this.audience,
});
return {
accessToken,
refreshToken,
expiresIn: payload.exp - now,
};
}
verifyAccessToken(token: string): TokenPayload {
try {
const decoded = jwt.verify(token, this.accessTokenSecret, {
algorithms: ['HS256'],
issuer: this.issuer,
audience: this.audience,
}) as any;
// Validate payload structure
const payload = TokenPayloadSchema.parse(decoded);
// Check if token is revoked
if (this.revokedTokens.has(payload.jti)) {
throw new Error('Token has been revoked');
}
return payload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Token expired');
} else if (error instanceof jwt.JsonWebTokenError) {
throw new Error('Invalid token');
}
throw error;
}
}
verifyRefreshToken(token: string): { sub: string; jti: string } {
try {
const decoded = jwt.verify(token, this.refreshTokenSecret, {
algorithms: ['HS256'],
issuer: this.issuer,
audience: this.audience,
}) as any;
if (this.revokedTokens.has(decoded.jti)) {
throw new Error('Refresh token has been revoked');
}
return {
sub: decoded.sub,
jti: decoded.jti,
};
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Refresh token expired');
}
throw new Error('Invalid refresh token');
}
}
revokeToken(jti: string): void {
this.revokedTokens.add(jti);
// In production, store in Redis or database
// this.redis.sadd('revoked_tokens', jti);
}
private generateJTI(): string {
return crypto.randomBytes(16).toString('hex');
}
private parseExpiry(expiry: string): number {
const match = expiry.match(/^(\d+)([smhd])$/);
if (!match) {
throw new Error('Invalid expiry format');
}
const value = parseInt(match[1], 10);
const unit = match[2];
switch (unit) {
case 's': return value;
case 'm': return value * 60;
case 'h': return value * 60 * 60;
case 'd': return value * 60 * 60 * 24;
default: throw new Error('Invalid expiry unit');
}
}
}
// Express middleware for JWT authentication
export function jwtAuthMiddleware(authManager: JWTAuthManager) {
return (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const payload = authManager.verifyAccessToken(token);
(req as any).user = payload;
next();
} catch (error) {
return res.status(401).json({ error: error.message });
}
};
}
// React hook for JWT authentication
export function useJWTAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState<TokenPayload | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('access_token');
if (token) {
try {
// Decode token without verification (for client-side)
const decoded = jwt.decode(token) as TokenPayload;
if (decoded && decoded.exp > Date.now() / 1000) {
setUser(decoded);
setIsAuthenticated(true);
} else {
// Token expired, try refresh
refreshToken();
}
} catch {
setIsAuthenticated(false);
}
}
setLoading(false);
}, []);
const login = async (credentials: { email: string; password: string }) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new Error('Login failed');
}
const { accessToken, refreshToken } = await response.json();
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
const decoded = jwt.decode(accessToken) as TokenPayload;
setUser(decoded);
setIsAuthenticated(true);
} catch (error) {
throw error;
}
};
const logout = () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
setUser(null);
setIsAuthenticated(false);
};
const refreshToken = async () => {
try {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token');
}
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const { accessToken } = await response.json();
localStorage.setItem('access_token', accessToken);
const decoded = jwt.decode(accessToken) as TokenPayload;
setUser(decoded);
setIsAuthenticated(true);
} catch {
logout();
}
};
return {
isAuthenticated,
user,
loading,
login,
logout,
refreshToken,
};
}
Role-Based Access Control (RBAC)
// src/auth/rbac.ts
interface Permission {
resource: string;
action: string;
conditions?: Record<string, any>;
}
interface Role {
name: string;
permissions: Permission[];
}
export class RBACManager {
private roles: Map<string, Role> = new Map();
private userRoles: Map<string, string[]> = new Map();
constructor() {
this.initializeDefaultRoles();
}
private initializeDefaultRoles(): void {
// Define default roles
this.addRole({
name: 'admin',
permissions: [
{ resource: '*', action: '*' }, // Full access
],
});
this.addRole({
name: 'editor',
permissions: [
{ resource: 'posts', action: 'create' },
{ resource: 'posts', action: 'read' },
{ resource: 'posts', action: 'update' },
{ resource: 'posts', action: 'delete', conditions: { own: true } },
{ resource: 'media', action: '*' },
],
});
this.addRole({
name: 'viewer',
permissions: [
{ resource: 'posts', action: 'read' },
{ resource: 'media', action: 'read' },
],
});
}
addRole(role: Role): void {
this.roles.set(role.name, role);
}
assignRole(userId: string, roleName: string): void {
const roles = this.userRoles.get(userId) || [];
if (!roles.includes(roleName)) {
roles.push(roleName);
this.userRoles.set(userId, roles);
}
}
removeRole(userId: string, roleName: string): void {
const roles = this.userRoles.get(userId) || [];
const filtered = roles.filter(r => r !== roleName);
this.userRoles.set(userId, filtered);
}
hasPermission(
userId: string,
resource: string,
action: string,
context?: Record<string, any>
): boolean {
const userRoleNames = this.userRoles.get(userId) || [];
for (const roleName of userRoleNames) {
const role = this.roles.get(roleName);
if (!role) continue;
for (const permission of role.permissions) {
if (this.matchesPermission(permission, resource, action, context)) {
return true;
}
}
}
return false;
}
private matchesPermission(
permission: Permission,
resource: string,
action: string,
context?: Record<string, any>
): boolean {
// Check resource match
if (permission.resource !== '*' && permission.resource !== resource) {
return false;
}
// Check action match
if (permission.action !== '*' && permission.action !== action) {
return false;
}
// Check conditions
if (permission.conditions && context) {
for (const [key, value] of Object.entries(permission.conditions)) {
if (context[key] !== value) {
return false;
}
}
}
return true;
}
getUserPermissions(userId: string): Permission[] {
const userRoleNames = this.userRoles.get(userId) || [];
const permissions: Permission[] = [];
for (const roleName of userRoleNames) {
const role = this.roles.get(roleName);
if (role) {
permissions.push(...role.permissions);
}
}
return permissions;
}
}
// React component for role-based access
export function RequirePermission({
resource,
action,
children,
fallback = null
}: {
resource: string;
action: string;
children: React.ReactNode;
fallback?: React.ReactNode;
}) {
const { user } = useJWTAuth();
const rbac = useContext(RBACContext);
if (!user || !rbac.hasPermission(user.sub, resource, action)) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// Hook for checking permissions
export function usePermission(resource: string, action: string): boolean {
const { user } = useJWTAuth();
const rbac = useContext(RBACContext);
if (!user) return false;
return rbac.hasPermission(user.sub, resource, action);
}
Security Headers Configuration
Implementing comprehensive security headers is crucial for protecting your application.
// src/security/headers.ts
export class SecurityHeaders {
static getProductionHeaders(): Record<string, string> {
return {
// CSP is handled separately with nonce support
// Strict Transport Security
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
// Prevent MIME type sniffing
'X-Content-Type-Options': 'nosniff',
// XSS Protection (legacy browsers)
'X-XSS-Protection': '1; mode=block',
// Clickjacking protection
'X-Frame-Options': 'DENY',
// Referrer Policy
'Referrer-Policy': 'strict-origin-when-cross-origin',
// Permissions Policy (formerly Feature Policy)
'Permissions-Policy': [
'accelerometer=()',
'autoplay=()',
'camera=()',
'encrypted-media=()',
'fullscreen=(self)',
'geolocation=(self)',
'gyroscope=()',
'magnetometer=()',
'microphone=()',
'midi=()',
'payment=()',
'picture-in-picture=()',
'sync-xhr=()',
'usb=()',
'interest-cohort=()', // Opt out of FLoC
].join(', '),
// Cross-Origin Policies
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
// Cache Control for sensitive pages
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
// Additional security headers
'X-DNS-Prefetch-Control': 'off',
'X-Download-Options': 'noopen',
'X-Permitted-Cross-Domain-Policies': 'none',
};
}
static getReportingEndpoints(): Record<string, string> {
return {
'Report-To': JSON.stringify([
{
group: 'csp-endpoint',
max_age: 86400,
endpoints: [{ url: '/api/security/csp-report' }],
},
{
group: 'network-errors',
max_age: 86400,
endpoints: [{ url: '/api/security/network-error' }],
},
{
group: 'deprecation',
max_age: 86400,
endpoints: [{ url: '/api/security/deprecation-report' }],
},
]),
'NEL': JSON.stringify({
report_to: 'network-errors',
max_age: 86400,
include_subdomains: true,
}),
};
}
static applyToResponse(res: Response, options?: {
noCacheHeaders?: boolean;
additionalHeaders?: Record<string, string>;
}): void {
const headers = this.getProductionHeaders();
const reportingHeaders = this.getReportingEndpoints();
// Apply security headers
for (const [name, value] of Object.entries(headers)) {
// Skip cache headers if requested
if (options?.noCacheHeaders && ['Cache-Control', 'Pragma', 'Expires'].includes(name)) {
continue;
}
res.setHeader(name, value);
}
// Apply reporting headers
for (const [name, value] of Object.entries(reportingHeaders)) {
res.setHeader(name, value);
}
// Apply additional headers
if (options?.additionalHeaders) {
for (const [name, value] of Object.entries(options.additionalHeaders)) {
res.setHeader(name, value);
}
}
// Remove potentially dangerous headers
res.removeHeader('X-Powered-By');
res.removeHeader('Server');
}
}
// Express middleware
export function securityHeadersMiddleware(options?: any) {
return (req: Request, res: Response, next: NextFunction) => {
SecurityHeaders.applyToResponse(res, options);
next();
};
}
Vulnerability Scanning and Testing
Implementing automated security testing ensures ongoing protection.
// src/security/vulnerabilityScanner.ts
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export class VulnerabilityScanner {
// Dependency scanning
static async scanDependencies(): Promise<{
vulnerabilities: any[];
summary: {
total: number;
high: number;
medium: number;
low: number;
};
}> {
try {
// Run npm audit
const { stdout } = await execAsync('npm audit --json');
const auditResult = JSON.parse(stdout);
const vulnerabilities = Object.values(auditResult.vulnerabilities || {});
const summary = {
total: vulnerabilities.length,
high: vulnerabilities.filter((v: any) => v.severity === 'high').length,
medium: vulnerabilities.filter((v: any) => v.severity === 'moderate').length,
low: vulnerabilities.filter((v: any) => v.severity === 'low').length,
};
return { vulnerabilities, summary };
} catch (error) {
console.error('Dependency scan failed:', error);
throw error;
}
}
// Static code analysis
static async scanCode(): Promise<{
issues: any[];
stats: Record<string, number>;
}> {
const issues: any[] = [];
const stats: Record<string, number> = {
dangerouslySetInnerHTML: 0,
eval: 0,
innerHTML: 0,
documentWrite: 0,
};
// Scan for dangerous patterns
const dangerousPatterns = [
{
pattern: /dangerouslySetInnerHTML/g,
type: 'dangerouslySetInnerHTML',
severity: 'high',
message: 'Use of dangerouslySetInnerHTML detected',
},
{
pattern: /eval\s*\(/g,
type: 'eval',
severity: 'critical',
message: 'Use of eval() detected',
},
{
pattern: /\.innerHTML\s*=/g,
type: 'innerHTML',
severity: 'high',
message: 'Direct innerHTML assignment detected',
},
{
pattern: /document\.write/g,
type: 'documentWrite',
severity: 'medium',
message: 'Use of document.write detected',
},
];
// Scan files
const { stdout } = await execAsync('find src -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx"');
const files = stdout.trim().split('\n');
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
for (const { pattern, type, severity, message } of dangerousPatterns) {
const matches = content.match(pattern);
if (matches) {
stats[type] += matches.length;
matches.forEach((match, index) => {
const line = content.substring(0, content.indexOf(match)).split('\n').length;
issues.push({
file,
line,
type,
severity,
message,
code: match,
});
});
}
}
}
return { issues, stats };
}
// CSP validation
static async validateCSP(url: string): Promise<{
valid: boolean;
issues: string[];
recommendations: string[];
}> {
const issues: string[] = [];
const recommendations: string[] = [];
try {
const response = await fetch(url, { method: 'HEAD' });
const cspHeader = response.headers.get('content-security-policy');
if (!cspHeader) {
issues.push('No Content-Security-Policy header found');
recommendations.push('Add a Content-Security-Policy header');
return { valid: false, issues, recommendations };
}
// Parse CSP
const directives = cspHeader.split(';').map(d => d.trim());
const directiveMap = new Map<string, string>();
for (const directive of directives) {
const [name, ...values] = directive.split(' ');
directiveMap.set(name, values.join(' '));
}
// Check for unsafe directives
if (directiveMap.get('script-src')?.includes("'unsafe-inline'")) {
issues.push("script-src contains 'unsafe-inline'");
recommendations.push("Use nonces or hashes instead of 'unsafe-inline'");
}
if (directiveMap.get('script-src')?.includes("'unsafe-eval'")) {
issues.push("script-src contains 'unsafe-eval'");
recommendations.push("Remove 'unsafe-eval' and refactor code to avoid eval()");
}
// Check for missing directives
const requiredDirectives = ['default-src', 'script-src', 'style-src', 'img-src'];
for (const required of requiredDirectives) {
if (!directiveMap.has(required)) {
issues.push(`Missing ${required} directive`);
recommendations.push(`Add ${required} directive to CSP`);
}
}
return {
valid: issues.length === 0,
issues,
recommendations,
};
} catch (error) {
issues.push(`Failed to validate CSP: ${error.message}`);
return { valid: false, issues, recommendations };
}
}
}
// Automated security testing
export async function runSecurityTests(): Promise<{
passed: boolean;
results: any;
}> {
const results = {
dependencies: await VulnerabilityScanner.scanDependencies(),
code: await VulnerabilityScanner.scanCode(),
csp: await VulnerabilityScanner.validateCSP(process.env.APP_URL || 'http://localhost:3000'),
};
const passed =
results.dependencies.summary.high === 0 &&
results.code.issues.filter(i => i.severity === 'critical').length === 0 &&
results.csp.valid;
return { passed, results };
}
Security Monitoring and Incident Response
// src/security/monitoring.ts
export class SecurityMonitor {
private incidents: SecurityIncident[] = [];
private alertHandlers: AlertHandler[] = [];
constructor() {
this.setupMonitoring();
}
private setupMonitoring(): void {
// Monitor for suspicious activities
this.monitorFailedLogins();
this.monitorCSPViolations();
this.monitorRateLimits();
this.monitorAnomalousPatterns();
}
private monitorFailedLogins(): void {
// Track failed login attempts
const failedAttempts = new Map<string, number>();
setInterval(() => {
for (const [ip, count] of failedAttempts) {
if (count > 5) {
this.createIncident({
type: 'brute-force',
severity: 'high',
description: `Multiple failed login attempts from ${ip}`,
metadata: { ip, attempts: count },
});
}
}
// Reset counters
failedAttempts.clear();
}, 60000); // Check every minute
}
private monitorCSPViolations(): void {
// Aggregate CSP violations
const violations = new Map<string, number>();
setInterval(() => {
for (const [directive, count] of violations) {
if (count > 100) {
this.createIncident({
type: 'csp-violation-spike',
severity: 'medium',
description: `Spike in CSP violations for ${directive}`,
metadata: { directive, count },
});
}
}
violations.clear();
}, 300000); // Check every 5 minutes
}
private monitorRateLimits(): void {
// Monitor for rate limit violations
const rateLimitViolations = new Map<string, number>();
setInterval(() => {
for (const [endpoint, count] of rateLimitViolations) {
if (count > 50) {
this.createIncident({
type: 'rate-limit-abuse',
severity: 'medium',
description: `Rate limit abuse on ${endpoint}`,
metadata: { endpoint, violations: count },
});
}
}
rateLimitViolations.clear();
}, 60000);
}
private monitorAnomalousPatterns(): void {
// Monitor for unusual patterns
setInterval(() => {
// Check for suspicious user agents
// Check for unusual request patterns
// Check for potential scanning activities
}, 300000);
}
createIncident(incident: Omit<SecurityIncident, 'id' | 'timestamp'>): void {
const fullIncident: SecurityIncident = {
id: generateId(),
timestamp: new Date(),
...incident,
};
this.incidents.push(fullIncident);
// Trigger alerts
for (const handler of this.alertHandlers) {
if (handler.shouldAlert(fullIncident)) {
handler.alert(fullIncident);
}
}
}
addAlertHandler(handler: AlertHandler): void {
this.alertHandlers.push(handler);
}
getIncidents(filter?: {
type?: string;
severity?: string;
since?: Date;
}): SecurityIncident[] {
return this.incidents.filter(incident => {
if (filter?.type && incident.type !== filter.type) return false;
if (filter?.severity && incident.severity !== filter.severity) return false;
if (filter?.since && incident.timestamp < filter.since) return false;
return true;
});
}
}
interface SecurityIncident {
id: string;
type: string;
severity: 'low' | 'medium' | 'high' | 'critical';
description: string;
timestamp: Date;
metadata?: any;
}
interface AlertHandler {
shouldAlert(incident: SecurityIncident): boolean;
alert(incident: SecurityIncident): void;
}
// Email alert handler
export class EmailAlertHandler implements AlertHandler {
constructor(private emailService: any) {}
shouldAlert(incident: SecurityIncident): boolean {
return incident.severity === 'high' || incident.severity === 'critical';
}
alert(incident: SecurityIncident): void {
this.emailService.send({
to: 'security@example.com',
subject: `Security Alert: ${incident.type}`,
body: `
Security incident detected:
Type: ${incident.type}
Severity: ${incident.severity}
Time: ${incident.timestamp}
Description: ${incident.description}
Metadata: ${JSON.stringify(incident.metadata, null, 2)}
`,
});
}
}
// Slack alert handler
export class SlackAlertHandler implements AlertHandler {
constructor(private webhookUrl: string) {}
shouldAlert(incident: SecurityIncident): boolean {
return true; // Alert on all incidents
}
async alert(incident: SecurityIncident): Promise<void> {
const color = {
low: '#36a64f',
medium: '#ff9900',
high: '#ff0000',
critical: '#800000',
}[incident.severity];
await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
color,
title: `Security Alert: ${incident.type}`,
text: incident.description,
fields: [
{
title: 'Severity',
value: incident.severity.toUpperCase(),
short: true,
},
{
title: 'Time',
value: incident.timestamp.toISOString(),
short: true,
},
],
footer: 'Security Monitor',
ts: Math.floor(incident.timestamp.getTime() / 1000),
}],
}),
});
}
}
Conclusion
Frontend security is a multi-layered challenge that requires constant vigilance and a comprehensive approach. Key takeaways from this guide include:
- Content Security Policy (CSP) is your first line of defense against XSS attacks - implement it with nonces for maximum security
- CORS configuration must be carefully managed to prevent unauthorized access while enabling legitimate cross-origin requests
- XSS prevention requires multiple strategies including input validation, output encoding, and safe DOM manipulation
- Authentication and authorization must be implemented with security-first principles, including proper token management and RBAC
- Security headers provide crucial browser-level protections that should be configured for all applications
- Continuous monitoring and testing ensure that security measures remain effective as your application evolves
Remember that security is not a one-time implementation but an ongoing process. Regular audits, updates, and monitoring are essential to maintain a robust security posture. By implementing these comprehensive security measures, you can significantly reduce the attack surface of your web applications and protect your users’ data and privacy.