Back to Blog

Frontend Web Security Architecture: A Production-Grade Guide

May 26, 2026231 min read0 views

Frontend Web Security Architecture: A Production-Grade Guide

Introduction: The Frontend Attack Surface

The frontend is the most exposed part of any web application—it executes untrusted code (JavaScript) in an untrusted environment (the browser) while handling sensitive user data. Modern SPAs have expanded this attack surface significantly:

┌─────────────────────────────────────────────────────────────────────────────┐
│                      FRONTEND ATTACK SURFACE                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ATTACK VECTORS                           TARGETS                           │
│  ──────────────                           ───────                           │
│  • XSS (Reflected, Stored, DOM)           • Session tokens                  │
│  • CSRF                                   • User credentials                │
│  • Clickjacking                           • Personal data (PII)             │
│  • Open Redirects                         • Financial information           │
│  • Prototype Pollution                    • API keys/secrets                │
│  • Supply Chain (npm)                     • Browser storage                 │
│  • Man-in-the-Middle                      • DOM manipulation                │
│  • CSS Injection                          • User actions                    │
│  • Tabnabbing                             • Clipboard data                  │
│  • WebSocket hijacking                    • Camera/microphone               │
│                                                                             │
│  OWASP TOP 10 (2021) - Frontend Relevant:                                  │
│  ─────────────────────────────────────────                                 │
│  A03: Injection (XSS)                     A07: Auth Failures               │
│  A05: Security Misconfiguration           A08: Software Integrity          │
│  A06: Vulnerable Components               A09: Logging Failures            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

This guide covers production-grade security patterns for modern frontend applications with real code examples, attack demonstrations, and defense implementations.


Part 1: Cross-Site Scripting (XSS)

Understanding XSS Attack Types

XSS allows attackers to execute malicious scripts in victims' browsers. It remains the most prevalent frontend vulnerability.

┌─────────────────────────────────────────────────────────────────────────────┐
│                           XSS ATTACK TYPES                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. REFLECTED XSS                                                          │
│  ┌─────────┐    malicious link    ┌─────────┐    reflected    ┌─────────┐ │
│  │ Attacker│ ─────────────────▶  │  Victim │ ─────────────▶  │ Server  │ │
│  └─────────┘                      └─────────┘                 └─────────┘ │
│       │                                │                           │       │
│       │                                │◀──── script in response ──│       │
│       │◀─────── stolen data ──────────│                                   │
│                                                                             │
│  2. STORED XSS                                                             │
│  ┌─────────┐   posts malicious    ┌─────────┐   stores        ┌────────┐ │
│  │ Attacker│ ──────────────────▶ │ Server  │ ───────────────▶│Database│ │
│  └─────────┘   comment/post       └─────────┘                 └────────┘ │
│                                        │                           │       │
│  ┌─────────┐   views page         ┌─────────┐   retrieves          │       │
│  │  Victim │ ──────────────────▶ │ Server  │ ◀─────────────────────│       │
│  └─────────┘                      └─────────┘                              │
│       │◀──── script executes ─────────│                                   │
│                                                                             │
│  3. DOM-BASED XSS                                                          │
│  ┌─────────┐                      ┌─────────┐                              │
│  │ Attacker│   crafted URL        │  Victim │   (Never touches server)    │
│  └─────────┘ ──────────────────▶ └─────────┘                              │
│                                        │                                   │
│                     Client-side JS reads URL/DOM and writes unsafely       │
│                                        │                                   │
│                                   Script executes                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

XSS Attack Examples

// VULNERABLE CODE EXAMPLES - DO NOT USE IN PRODUCTION

// 1. Reflected XSS via URL parameter
// URL: https://example.com/search?q=<script>alert(document.cookie)</script>
function VulnerableSearch() {
  const params = new URLSearchParams(window.location.search);
  const query = params.get('q');

  // ❌ VULNERABLE: Direct HTML insertion
  return <div dangerouslySetInnerHTML={{ __html: `Results for: ${query}` }} />;
}

// 2. DOM-based XSS via innerHTML
function VulnerableComment({ comment }: { comment: string }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // ❌ VULNERABLE: innerHTML with user content
    ref.current!.innerHTML = comment;
  }, [comment]);

  return <div ref={ref} />;
}

// 3. XSS via href/src attributes
function VulnerableLink({ url }: { url: string }) {
  // ❌ VULNERABLE: javascript: protocol not blocked
  // Attacker input: "javascript:alert(document.cookie)"
  return <a href={url}>Click here</a>;
}

// 4. XSS via event handlers
function VulnerableAvatar({ imageUrl, onError }: AvatarProps) {
  // ❌ VULNERABLE: Attacker-controlled event handler
  // Attacker input for onError: "alert(document.cookie)"
  return <img src={imageUrl} onError={onError} />;
}

// 5. XSS via JSON injection
function VulnerableEmbed({ userData }: { userData: object }) {
  // ❌ VULNERABLE: Script context injection
  // If userData contains </script><script>alert(1)</script>
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `window.__DATA__ = ${JSON.stringify(userData)}`
      }}
    />
  );
}

XSS Defense: Input Sanitization

// DOMPurify - The gold standard for HTML sanitization
import DOMPurify from 'dompurify';

// Configure DOMPurify for strict sanitization
const purifyConfig: DOMPurify.Config = {
  // Allowed tags
  ALLOWED_TAGS: [
    'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li',
    'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre'
  ],

  // Allowed attributes
  ALLOWED_ATTR: ['href', 'title', 'target', 'rel'],

  // Only allow safe href protocols
  ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i,

  // Force target="_blank" links to have rel="noopener"
  ADD_ATTR: ['target'],

  // Hooks for additional processing
  RETURN_DOM: false,
  RETURN_DOM_FRAGMENT: false,
};

// Sanitization utility
export function sanitizeHTML(dirty: string): string {
  return DOMPurify.sanitize(dirty, purifyConfig);
}

// Hook for safe HTML rendering
export function useSanitizedHTML(html: string): string {
  return useMemo(() => sanitizeHTML(html), [html]);
}

// Safe component for user-generated HTML content
interface SafeHTMLProps {
  html: string;
  className?: string;
  allowedTags?: string[];
}

export function SafeHTML({ html, className, allowedTags }: SafeHTMLProps) {
  const sanitized = useMemo(() => {
    const config = allowedTags
      ? { ...purifyConfig, ALLOWED_TAGS: allowedTags }
      : purifyConfig;

    return DOMPurify.sanitize(html, config);
  }, [html, allowedTags]);

  return (
    <div
      className={className}
      dangerouslySetInnerHTML={{ __html: sanitized }}
    />
  );
}

// Add DOMPurify hooks for extra security
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  // Force all links to open in new tab safely
  if (node.tagName === 'A') {
    node.setAttribute('target', '_blank');
    node.setAttribute('rel', 'noopener noreferrer');
  }

  // Remove any data-* attributes (potential for attacks)
  Array.from(node.attributes).forEach(attr => {
    if (attr.name.startsWith('data-')) {
      node.removeAttribute(attr.name);
    }
  });
});

XSS Defense: Context-Aware Output Encoding

// Different contexts require different encoding

// 1. HTML context encoding
export function encodeHTML(str: string): string {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// 2. Attribute context encoding
export function encodeAttribute(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

// 3. JavaScript context encoding
export function encodeJavaScript(str: string): string {
  return str
    .replace(/\\/g, '\\\\')
    .replace(/'/g, "\\'")
    .replace(/"/g, '\\"')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r')
    .replace(/\t/g, '\\t')
    .replace(/<\//g, '<\\/'); // Prevent </script> injection
}

// 4. URL context encoding
export function encodeURL(str: string): string {
  return encodeURIComponent(str);
}

// 5. CSS context encoding
export function encodeCSS(str: string): string {
  return str.replace(/[<>"'`\\]/g, char => `\\${char.charCodeAt(0).toString(16)} `);
}

// Safe JSON embedding in script tags
export function safeJSONEmbed(data: unknown): string {
  // Encode characters that could break out of script context
  return JSON.stringify(data)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
    .replace(/'/g, '\\u0027')
    .replace(/"/g, '\\u0022');
}

// Server-side: Safe data embedding component
function SafeDataEmbed({ data, variableName }: { data: unknown; variableName: string }) {
  const safeData = safeJSONEmbed(data);

  return (
    <script
      id="app-data"
      type="application/json"
      dangerouslySetInnerHTML={{ __html: safeData }}
    />
  );
}

// Client-side: Read embedded data safely
function readEmbeddedData<T>(): T {
  const script = document.getElementById('app-data');
  if (!script) throw new Error('App data not found');

  return JSON.parse(script.textContent || '{}');
}

XSS Defense: URL Validation

// Prevent javascript: and data: URL attacks

const SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:'];

export function isSafeURL(url: string): boolean {
  try {
    const parsed = new URL(url, window.location.origin);
    return SAFE_URL_PROTOCOLS.includes(parsed.protocol);
  } catch {
    // Relative URLs are safe
    return !url.toLowerCase().startsWith('javascript:') &&
           !url.toLowerCase().startsWith('data:') &&
           !url.toLowerCase().startsWith('vbscript:');
  }
}

export function sanitizeURL(url: string): string {
  if (isSafeURL(url)) {
    return url;
  }
  return '#'; // Safe fallback
}

// Safe link component
interface SafeLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  href: string;
  children: React.ReactNode;
}

export function SafeLink({ href, children, ...props }: SafeLinkProps) {
  const safeHref = useMemo(() => sanitizeURL(href), [href]);

  // External links get security attributes
  const isExternal = useMemo(() => {
    try {
      const url = new URL(href, window.location.origin);
      return url.origin !== window.location.origin;
    } catch {
      return false;
    }
  }, [href]);

  return (
    <a
      href={safeHref}
      {...props}
      {...(isExternal && {
        target: '_blank',
        rel: 'noopener noreferrer',
      })}
    >
      {children}
    </a>
  );
}

// Safe redirect function
export function safeRedirect(url: string, allowedDomains: string[] = []): void {
  try {
    const parsed = new URL(url, window.location.origin);

    // Check protocol
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      console.error('Unsafe redirect protocol:', parsed.protocol);
      return;
    }

    // Check domain whitelist for external redirects
    if (parsed.origin !== window.location.origin) {
      const isAllowed = allowedDomains.some(domain =>
        parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`)
      );

      if (!isAllowed) {
        console.error('Redirect to non-whitelisted domain:', parsed.hostname);
        return;
      }
    }

    window.location.href = url;
  } catch (error) {
    console.error('Invalid redirect URL:', url);
  }
}

Part 2: Content Security Policy (CSP)

Understanding CSP

CSP is a powerful defense-in-depth mechanism that restricts what resources can be loaded and executed.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CONTENT SECURITY POLICY DIRECTIVES                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  DIRECTIVE          CONTROLS                    EXAMPLE VALUE               │
│  ─────────────────────────────────────────────────────────────────────────  │
│  default-src        Fallback for other          'self'                      │
│  script-src         JavaScript sources          'self' 'nonce-xxx'          │
│  style-src          CSS sources                 'self' 'unsafe-inline'      │
│  img-src            Image sources               'self' data: https:         │
│  font-src           Font sources                'self' https://fonts.com    │
│  connect-src        XHR/Fetch/WebSocket         'self' https://api.com      │
│  frame-src          iframe sources              'none'                      │
│  frame-ancestors    Who can embed us            'none' (clickjacking)       │
│  form-action        Form submission targets     'self'                      │
│  base-uri           <base> tag restrictions     'self'                      │
│  object-src         Plugins (Flash, etc)        'none'                      │
│  report-uri         Violation reporting         /csp-report                 │
│  report-to          Reporting API endpoint      csp-endpoint                │
│                                                                             │
│  SOURCE VALUES:                                                             │
│  ─────────────────────────────────────────────────────────────────────────  │
│  'self'             Same origin                                             │
│  'none'             Block all                                               │
│  'unsafe-inline'    Allow inline (defeats XSS protection!)                  │
│  'unsafe-eval'      Allow eval() (dangerous!)                               │
│  'nonce-{random}'   Allow specific inline scripts with nonce                │
│  'sha256-{hash}'    Allow scripts matching hash                             │
│  'strict-dynamic'   Trust scripts loaded by trusted scripts                 │
│  https:             Any HTTPS origin                                        │
│  data:              Data URIs                                               │
│  blob:              Blob URIs                                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Production CSP Implementation

// CSP nonce generation and management

import { randomBytes } from 'crypto';

// Generate cryptographically secure nonce
export function generateNonce(): string {
  return randomBytes(16).toString('base64');
}

// CSP configuration builder
interface CSPConfig {
  nonce: string;
  reportUri?: string;
  reportOnly?: boolean;
  isDevelopment?: boolean;
}

export function buildCSP(config: CSPConfig): string {
  const { nonce, reportUri, reportOnly, isDevelopment } = config;

  const directives: Record<string, string[]> = {
    'default-src': ["'self'"],

    // Scripts: nonce-based for strict CSP
    'script-src': [
      "'self'",
      `'nonce-${nonce}'`,
      "'strict-dynamic'", // Allow scripts loaded by nonced scripts
      // Fallback for browsers without strict-dynamic support
      'https:',
    ],

    // Styles: nonce for inline, self for external
    'style-src': [
      "'self'",
      `'nonce-${nonce}'`,
      // Allow inline styles from CSS-in-JS libraries (use hash in production)
      ...(isDevelopment ? ["'unsafe-inline'"] : []),
    ],

    // Images: self + data URIs for inline images + CDN
    'img-src': [
      "'self'",
      'data:',
      'blob:',
      'https://cdn.example.com',
      'https://*.cloudinary.com',
    ],

    // Fonts: self + Google Fonts + CDN
    'font-src': [
      "'self'",
      'https://fonts.gstatic.com',
      'https://cdn.example.com',
    ],

    // API connections
    'connect-src': [
      "'self'",
      'https://api.example.com',
      'https://analytics.example.com',
      // WebSocket
      'wss://ws.example.com',
      ...(isDevelopment ? ['ws://localhost:*'] : []),
    ],

    // Frames: none by default (clickjacking protection)
    'frame-src': ["'none'"],
    'frame-ancestors': ["'none'"],

    // Form submissions
    'form-action': ["'self'"],

    // Base URI (prevent base tag injection)
    'base-uri': ["'self'"],

    // Block plugins
    'object-src': ["'none'"],

    // Upgrade HTTP to HTTPS
    'upgrade-insecure-requests': [],

    // Block mixed content
    'block-all-mixed-content': [],
  };

  // Add reporting
  if (reportUri) {
    directives['report-uri'] = [reportUri];
    directives['report-to'] = ['csp-endpoint'];
  }

  // Build CSP string
  const csp = Object.entries(directives)
    .map(([directive, values]) =>
      values.length > 0 ? `${directive} ${values.join(' ')}` : directive
    )
    .join('; ');

  return csp;
}

// Express middleware for CSP
import { Request, Response, NextFunction } from 'express';

export function cspMiddleware(options: Partial<CSPConfig> = {}) {
  return (req: Request, res: Response, next: NextFunction) => {
    // Generate nonce per request
    const nonce = generateNonce();

    // Store nonce for use in templates
    res.locals.cspNonce = nonce;

    // Build CSP header
    const csp = buildCSP({
      nonce,
      reportUri: '/api/csp-report',
      isDevelopment: process.env.NODE_ENV === 'development',
      ...options,
    });

    // Set header (use Report-Only in initial rollout)
    const headerName = options.reportOnly
      ? 'Content-Security-Policy-Report-Only'
      : 'Content-Security-Policy';

    res.setHeader(headerName, csp);

    next();
  };
}

// CSP violation report handler
interface CSPViolationReport {
  'csp-report': {
    'document-uri': string;
    'referrer': string;
    'blocked-uri': string;
    'violated-directive': string;
    'original-policy': string;
    'source-file'?: string;
    'line-number'?: number;
    'column-number'?: number;
  };
}

app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body as CSPViolationReport;

  // Log violation (filter out noise from browser extensions)
  const blockedUri = report['csp-report']['blocked-uri'];
  const isExtensionNoise = blockedUri.startsWith('chrome-extension:') ||
                           blockedUri.startsWith('moz-extension:');

  if (!isExtensionNoise) {
    console.warn('CSP Violation:', {
      documentUri: report['csp-report']['document-uri'],
      blockedUri,
      violatedDirective: report['csp-report']['violated-directive'],
      sourceFile: report['csp-report']['source-file'],
      lineNumber: report['csp-report']['line-number'],
    });

    // Send to monitoring service
    sendToMonitoring('csp_violation', report);
  }

  res.status(204).end();
});

CSP with React/Next.js

// Next.js: Nonce-based CSP

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

  // CSP header
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data: https://cdn.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';
    upgrade-insecure-requests;
  `.replace(/\s{2,}/g, ' ').trim();

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });

  response.headers.set('Content-Security-Policy', cspHeader);

  return response;
}

// app/layout.tsx
import { headers } from 'next/headers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const nonce = headers().get('x-nonce') || '';

  return (
    <html lang="en">
      <head>
        {/* Inline scripts must have nonce */}
        <script
          nonce={nonce}
          dangerouslySetInnerHTML={{
            __html: `console.log('This script is allowed by CSP')`,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

// For styled-components or emotion with nonce
// _document.tsx (Pages Router) or provider setup
import { ServerStyleSheet } from 'styled-components';

export default function Document() {
  const nonce = headers().get('x-nonce') || '';

  return (
    <Html>
      <Head nonce={nonce}>
        <style nonce={nonce}>{/* critical CSS */}</style>
      </Head>
      <body>
        <Main />
        <NextScript nonce={nonce} />
      </body>
    </Html>
  );
}

Part 3: Cross-Site Request Forgery (CSRF)

Understanding CSRF

CSRF tricks authenticated users into performing unwanted actions.

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CSRF ATTACK FLOW                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. User logs into bank.com (session cookie set)                           │
│                                                                             │
│  2. User visits evil.com (in another tab)                                  │
│                                                                             │
│  3. evil.com contains:                                                     │
│     <form action="https://bank.com/transfer" method="POST">                │
│       <input name="to" value="attacker">                                   │
│       <input name="amount" value="10000">                                  │
│     </form>                                                                 │
│     <script>document.forms[0].submit()</script>                            │
│                                                                             │
│  4. Browser automatically includes bank.com cookies                        │
│     (because request goes TO bank.com)                                     │
│                                                                             │
│  5. Bank processes transfer (thinks user initiated it)                     │
│                                                                             │
│  WHY IT WORKS:                                                             │
│  • Cookies are sent automatically to their origin                          │
│  • Server can't distinguish legitimate vs forged request                   │
│  • Same-origin policy doesn't prevent sending requests, only reading       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

CSRF Defense: Token-Based Protection

// Server-side CSRF token generation and validation

import { randomBytes, createHmac } from 'crypto';

const CSRF_SECRET = process.env.CSRF_SECRET!;
const CSRF_TOKEN_EXPIRY = 60 * 60 * 1000; // 1 hour

interface CSRFToken {
  token: string;
  timestamp: number;
}

// Generate CSRF token tied to session
export function generateCSRFToken(sessionId: string): string {
  const timestamp = Date.now();
  const random = randomBytes(16).toString('hex');

  // Create token: random + timestamp
  const tokenData = `${random}.${timestamp}`;

  // Sign with HMAC to prevent tampering
  const signature = createHmac('sha256', CSRF_SECRET)
    .update(`${sessionId}.${tokenData}`)
    .digest('hex');

  return `${tokenData}.${signature}`;
}

// Validate CSRF token
export function validateCSRFToken(token: string, sessionId: string): boolean {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return false;

    const [random, timestamp, signature] = parts;
    const tokenData = `${random}.${timestamp}`;

    // Verify signature
    const expectedSignature = createHmac('sha256', CSRF_SECRET)
      .update(`${sessionId}.${tokenData}`)
      .digest('hex');

    // Timing-safe comparison
    if (!timingSafeEqual(signature, expectedSignature)) {
      return false;
    }

    // Check expiry
    const tokenTime = parseInt(timestamp, 10);
    if (Date.now() - tokenTime > CSRF_TOKEN_EXPIRY) {
      return false;
    }

    return true;
  } catch {
    return false;
  }
}

// Timing-safe string comparison
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;

  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return result === 0;
}

// Express middleware
export function csrfProtection() {
  return (req: Request, res: Response, next: NextFunction) => {
    // Skip for safe methods
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      // Generate token for forms
      const token = generateCSRFToken(req.sessionID);
      res.locals.csrfToken = token;
      return next();
    }

    // Validate token for state-changing methods
    const token = req.headers['x-csrf-token'] as string ||
                  req.body?._csrf;

    if (!token || !validateCSRFToken(token, req.sessionID)) {
      return res.status(403).json({
        error: 'CSRF validation failed',
        code: 'CSRF_ERROR',
      });
    }

    next();
  };
}

CSRF Defense: SameSite Cookies

// Modern CSRF protection with SameSite cookies

// Session cookie configuration
const sessionConfig = {
  name: 'session',
  secret: process.env.SESSION_SECRET!,
  cookie: {
    // Strict: Cookie only sent for same-site requests
    // Lax: Cookie sent for same-site + top-level navigations
    sameSite: 'strict' as const,

    // Only send over HTTPS
    secure: process.env.NODE_ENV === 'production',

    // Prevent JavaScript access
    httpOnly: true,

    // Set appropriate domain
    domain: process.env.COOKIE_DOMAIN,

    // Path restriction
    path: '/',

    // Expiry
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
};

// For APIs: Double Submit Cookie pattern
export function doubleSubmitCookie() {
  return (req: Request, res: Response, next: NextFunction) => {
    // For GET requests, set the CSRF cookie
    if (req.method === 'GET') {
      const csrfToken = randomBytes(32).toString('hex');

      res.cookie('csrf-token', csrfToken, {
        sameSite: 'strict',
        secure: process.env.NODE_ENV === 'production',
        httpOnly: false, // Must be readable by JavaScript
        path: '/',
      });

      return next();
    }

    // For state-changing requests, verify header matches cookie
    const cookieToken = req.cookies['csrf-token'];
    const headerToken = req.headers['x-csrf-token'];

    if (!cookieToken || !headerToken || cookieToken !== headerToken) {
      return res.status(403).json({ error: 'CSRF validation failed' });
    }

    next();
  };
}

// React hook for CSRF tokens
function useCSRFToken(): string {
  const [token, setToken] = useState<string>('');

  useEffect(() => {
    // Read from cookie
    const cookie = document.cookie
      .split('; ')
      .find(row => row.startsWith('csrf-token='));

    if (cookie) {
      setToken(cookie.split('=')[1]);
    }
  }, []);

  return token;
}

// API client with CSRF protection
class SecureAPIClient {
  private csrfToken: string = '';

  async request<T>(url: string, options: RequestInit = {}): Promise<T> {
    // Get CSRF token from cookie
    this.csrfToken = this.getCSRFToken();

    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'X-CSRF-Token': this.csrfToken,
        'Content-Type': 'application/json',
      },
      credentials: 'same-origin', // Include cookies
    });

    if (!response.ok) {
      throw new APIError(response.status, await response.json());
    }

    return response.json();
  }

  private getCSRFToken(): string {
    const match = document.cookie.match(/csrf-token=([^;]+)/);
    return match ? match[1] : '';
  }
}

Part 4: Authentication Security

Secure Session Management

// Secure session implementation

interface SessionConfig {
  sessionDuration: number;
  absoluteTimeout: number;
  slidingWindow: boolean;
  regenerateOnAuth: boolean;
}

const DEFAULT_SESSION_CONFIG: SessionConfig = {
  sessionDuration: 30 * 60 * 1000,    // 30 minutes
  absoluteTimeout: 8 * 60 * 60 * 1000, // 8 hours max
  slidingWindow: true,
  regenerateOnAuth: true,
};

class SecureSessionManager {
  private config: SessionConfig;

  constructor(config: Partial<SessionConfig> = {}) {
    this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
  }

  // Create new session after authentication
  async createSession(userId: string, req: Request, res: Response): Promise<Session> {
    // Generate secure session ID
    const sessionId = await this.generateSecureId();

    const session: Session = {
      id: sessionId,
      userId,
      createdAt: Date.now(),
      lastActivity: Date.now(),
      expiresAt: Date.now() + this.config.sessionDuration,
      absoluteExpiry: Date.now() + this.config.absoluteTimeout,
      // Bind session to client fingerprint for extra security
      fingerprint: this.generateFingerprint(req),
      // Track auth events
      authEvents: [{
        type: 'login',
        timestamp: Date.now(),
        ip: req.ip,
        userAgent: req.headers['user-agent'],
      }],
    };

    // Store session server-side (Redis/database)
    await this.storeSession(session);

    // Set secure session cookie
    this.setSessionCookie(res, sessionId);

    return session;
  }

  // Validate and refresh session
  async validateSession(req: Request, res: Response): Promise<Session | null> {
    const sessionId = req.cookies['session'];
    if (!sessionId) return null;

    const session = await this.getSession(sessionId);
    if (!session) return null;

    // Check expiry
    if (Date.now() > session.expiresAt) {
      await this.destroySession(sessionId);
      return null;
    }

    // Check absolute timeout
    if (Date.now() > session.absoluteExpiry) {
      await this.destroySession(sessionId);
      return null;
    }

    // Verify fingerprint (detect session hijacking)
    const currentFingerprint = this.generateFingerprint(req);
    if (session.fingerprint !== currentFingerprint) {
      console.warn('Session fingerprint mismatch', {
        sessionId,
        expected: session.fingerprint,
        actual: currentFingerprint,
      });
      // Could be suspicious - log but don't immediately invalidate
      // (user might have changed networks)
    }

    // Sliding window: extend expiry on activity
    if (this.config.slidingWindow) {
      session.lastActivity = Date.now();
      session.expiresAt = Date.now() + this.config.sessionDuration;
      await this.storeSession(session);
    }

    return session;
  }

  // Regenerate session ID (after auth level changes)
  async regenerateSession(oldSessionId: string, res: Response): Promise<string> {
    const session = await this.getSession(oldSessionId);
    if (!session) throw new Error('Session not found');

    // Create new session ID
    const newSessionId = await this.generateSecureId();

    // Update session with new ID
    await this.deleteSession(oldSessionId);
    session.id = newSessionId;
    session.authEvents.push({
      type: 'session_regenerated',
      timestamp: Date.now(),
    });
    await this.storeSession(session);

    // Update cookie
    this.setSessionCookie(res, newSessionId);

    return newSessionId;
  }

  // Destroy session (logout)
  async destroySession(sessionId: string, res?: Response): Promise<void> {
    await this.deleteSession(sessionId);

    if (res) {
      res.clearCookie('session', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        path: '/',
      });
    }
  }

  // Destroy all sessions for user (password change, etc.)
  async destroyAllUserSessions(userId: string): Promise<void> {
    const sessions = await this.getUserSessions(userId);
    await Promise.all(sessions.map(s => this.deleteSession(s.id)));
  }

  private async generateSecureId(): Promise<string> {
    return randomBytes(32).toString('hex');
  }

  private generateFingerprint(req: Request): string {
    // Create fingerprint from client characteristics
    const components = [
      req.headers['user-agent'] || '',
      req.headers['accept-language'] || '',
      // Don't include IP - too volatile
    ];

    return createHash('sha256')
      .update(components.join('|'))
      .digest('hex');
  }

  private setSessionCookie(res: Response, sessionId: string): void {
    res.cookie('session', sessionId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      path: '/',
      maxAge: this.config.absoluteTimeout,
    });
  }

  // Implement these with your storage backend (Redis, etc.)
  private async storeSession(session: Session): Promise<void> { /* ... */ }
  private async getSession(sessionId: string): Promise<Session | null> { /* ... */ }
  private async deleteSession(sessionId: string): Promise<void> { /* ... */ }
  private async getUserSessions(userId: string): Promise<Session[]> { /* ... */ }
}

JWT Security

// Secure JWT implementation

import { SignJWT, jwtVerify, JWTPayload } from 'jose';

interface TokenConfig {
  accessTokenExpiry: string;
  refreshTokenExpiry: string;
  issuer: string;
  audience: string;
}

const JWT_CONFIG: TokenConfig = {
  accessTokenExpiry: '15m',      // Short-lived
  refreshTokenExpiry: '7d',      // Longer for refresh
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
};

class JWTService {
  private accessSecret: Uint8Array;
  private refreshSecret: Uint8Array;

  constructor() {
    // Use different secrets for access and refresh tokens
    this.accessSecret = new TextEncoder().encode(process.env.JWT_ACCESS_SECRET!);
    this.refreshSecret = new TextEncoder().encode(process.env.JWT_REFRESH_SECRET!);
  }

  // Generate token pair
  async generateTokens(user: User): Promise<TokenPair> {
    const accessToken = await this.generateAccessToken(user);
    const refreshToken = await this.generateRefreshToken(user);

    return { accessToken, refreshToken };
  }

  private async generateAccessToken(user: User): Promise<string> {
    return new SignJWT({
      sub: user.id,
      email: user.email,
      role: user.role,
      // Don't include sensitive data!
    })
      .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
      .setIssuedAt()
      .setIssuer(JWT_CONFIG.issuer)
      .setAudience(JWT_CONFIG.audience)
      .setExpirationTime(JWT_CONFIG.accessTokenExpiry)
      // Add unique identifier for revocation
      .setJti(crypto.randomUUID())
      .sign(this.accessSecret);
  }

  private async generateRefreshToken(user: User): Promise<string> {
    const tokenId = crypto.randomUUID();

    // Store refresh token ID for revocation
    await this.storeRefreshToken(tokenId, user.id);

    return new SignJWT({
      sub: user.id,
      type: 'refresh',
    })
      .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
      .setIssuedAt()
      .setIssuer(JWT_CONFIG.issuer)
      .setAudience(JWT_CONFIG.audience)
      .setExpirationTime(JWT_CONFIG.refreshTokenExpiry)
      .setJti(tokenId)
      .sign(this.refreshSecret);
  }

  // Verify access token
  async verifyAccessToken(token: string): Promise<JWTPayload> {
    try {
      const { payload } = await jwtVerify(token, this.accessSecret, {
        issuer: JWT_CONFIG.issuer,
        audience: JWT_CONFIG.audience,
      });

      // Check if token is revoked (optional - performance tradeoff)
      if (payload.jti && await this.isTokenRevoked(payload.jti)) {
        throw new Error('Token revoked');
      }

      return payload;
    } catch (error) {
      throw new AuthenticationError('Invalid access token');
    }
  }

  // Refresh token flow
  async refreshTokens(refreshToken: string): Promise<TokenPair> {
    try {
      const { payload } = await jwtVerify(refreshToken, this.refreshSecret, {
        issuer: JWT_CONFIG.issuer,
        audience: JWT_CONFIG.audience,
      });

      // Verify it's a refresh token
      if (payload.type !== 'refresh') {
        throw new Error('Not a refresh token');
      }

      // Check if refresh token is revoked
      if (payload.jti && !(await this.isRefreshTokenValid(payload.jti))) {
        throw new Error('Refresh token revoked');
      }

      // Get user and generate new tokens
      const user = await this.getUserById(payload.sub as string);

      // Rotate refresh token (revoke old, issue new)
      await this.revokeRefreshToken(payload.jti as string);

      return this.generateTokens(user);
    } catch (error) {
      throw new AuthenticationError('Invalid refresh token');
    }
  }

  // Revoke all tokens for user
  async revokeAllUserTokens(userId: string): Promise<void> {
    await this.revokeUserRefreshTokens(userId);
    // Access tokens will expire naturally (short-lived)
    // For immediate revocation, maintain a blacklist
  }

  // Storage methods (implement with Redis/database)
  private async storeRefreshToken(tokenId: string, userId: string): Promise<void> { /* ... */ }
  private async isRefreshTokenValid(tokenId: string): Promise<boolean> { /* ... */ }
  private async revokeRefreshToken(tokenId: string): Promise<void> { /* ... */ }
  private async revokeUserRefreshTokens(userId: string): Promise<void> { /* ... */ }
  private async isTokenRevoked(tokenId: string): Promise<boolean> { /* ... */ }
  private async getUserById(userId: string): Promise<User> { /* ... */ }
}

// Frontend: Secure token storage
class TokenStorage {
  // ❌ DON'T store tokens in localStorage (XSS vulnerable)
  // ❌ DON'T store tokens in sessionStorage (XSS vulnerable)
  // ✅ DO store tokens in httpOnly cookies (server-set)
  // ✅ OR use in-memory storage (for SPAs with short sessions)

  private accessToken: string | null = null;

  setAccessToken(token: string): void {
    // In-memory only - lost on refresh but safe from XSS
    this.accessToken = token;
  }

  getAccessToken(): string | null {
    return this.accessToken;
  }

  clearTokens(): void {
    this.accessToken = null;
  }
}

// API client with token refresh
class AuthenticatedAPIClient {
  private tokenStorage = new TokenStorage();
  private refreshPromise: Promise<void> | null = null;

  async request<T>(url: string, options: RequestInit = {}): Promise<T> {
    const accessToken = this.tokenStorage.getAccessToken();

    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: accessToken ? `Bearer ${accessToken}` : '',
      },
      credentials: 'include', // Include refresh token cookie
    });

    // Token expired - try refresh
    if (response.status === 401) {
      await this.refreshTokens();
      return this.request(url, options); // Retry
    }

    if (!response.ok) {
      throw new APIError(response.status, await response.json());
    }

    return response.json();
  }

  private async refreshTokens(): Promise<void> {
    // Prevent concurrent refresh requests
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = (async () => {
      try {
        const response = await fetch('/api/auth/refresh', {
          method: 'POST',
          credentials: 'include',
        });

        if (!response.ok) {
          throw new Error('Refresh failed');
        }

        const { accessToken } = await response.json();
        this.tokenStorage.setAccessToken(accessToken);
      } catch (error) {
        this.tokenStorage.clearTokens();
        // Redirect to login
        window.location.href = '/login';
      } finally {
        this.refreshPromise = null;
      }
    })();

    return this.refreshPromise;
  }
}

Part 5: Secure Data Handling

Client-Side Storage Security

// Secure storage patterns

// ❌ NEVER store sensitive data in localStorage/sessionStorage
// XSS can read it: localStorage.getItem('token')

// For non-sensitive data that must persist
class SecureLocalStorage {
  private prefix = 'app_';
  private encryptionKey: CryptoKey | null = null;

  async init(userKey: string): Promise<void> {
    // Derive encryption key from user-specific data
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(userKey),
      'PBKDF2',
      false,
      ['deriveKey']
    );

    this.encryptionKey = await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: new TextEncoder().encode('secure-storage-salt'),
        iterations: 100000,
        hash: 'SHA-256',
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  async setItem(key: string, value: unknown): Promise<void> {
    if (!this.encryptionKey) throw new Error('Storage not initialized');

    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encoded = new TextEncoder().encode(JSON.stringify(value));

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey,
      encoded
    );

    // Store IV + encrypted data
    const combined = new Uint8Array(iv.length + encrypted.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encrypted), iv.length);

    localStorage.setItem(
      this.prefix + key,
      btoa(String.fromCharCode(...combined))
    );
  }

  async getItem<T>(key: string): Promise<T | null> {
    if (!this.encryptionKey) throw new Error('Storage not initialized');

    const stored = localStorage.getItem(this.prefix + key);
    if (!stored) return null;

    try {
      const combined = new Uint8Array(
        atob(stored).split('').map(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.encryptionKey,
        encrypted
      );

      return JSON.parse(new TextDecoder().decode(decrypted));
    } catch {
      // Corrupted or tampered - remove it
      this.removeItem(key);
      return null;
    }
  }

  removeItem(key: string): void {
    localStorage.removeItem(this.prefix + key);
  }

  clear(): void {
    Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix))
      .forEach(key => localStorage.removeItem(key));
  }
}

// IndexedDB with encryption for larger data
class SecureIndexedDB {
  private db: IDBDatabase | null = null;
  private encryptionKey: CryptoKey | null = null;
  private dbName = 'secure-app-db';
  private storeName = 'encrypted-store';

  async init(userKey: string): Promise<void> {
    // Initialize encryption key
    await this.initEncryption(userKey);

    // Open database
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, { keyPath: 'id' });
        }
      };
    });
  }

  private async initEncryption(userKey: string): Promise<void> {
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(userKey),
      'PBKDF2',
      false,
      ['deriveKey']
    );

    this.encryptionKey = await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: new TextEncoder().encode('indexeddb-salt'),
        iterations: 100000,
        hash: 'SHA-256',
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  async put(id: string, data: unknown): Promise<void> {
    if (!this.db || !this.encryptionKey) throw new Error('DB not initialized');

    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encoded = new TextEncoder().encode(JSON.stringify(data));

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey,
      encoded
    );

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(this.storeName, 'readwrite');
      const store = transaction.objectStore(this.storeName);

      const request = store.put({
        id,
        iv: Array.from(iv),
        data: Array.from(new Uint8Array(encrypted)),
      });

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve();
    });
  }

  async get<T>(id: string): Promise<T | null> {
    if (!this.db || !this.encryptionKey) throw new Error('DB not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(this.storeName, 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.get(id);

      request.onerror = () => reject(request.error);
      request.onsuccess = async () => {
        const result = request.result;
        if (!result) {
          resolve(null);
          return;
        }

        try {
          const iv = new Uint8Array(result.iv);
          const encrypted = new Uint8Array(result.data);

          const decrypted = await crypto.subtle.decrypt(
            { name: 'AES-GCM', iv },
            this.encryptionKey!,
            encrypted
          );

          resolve(JSON.parse(new TextDecoder().decode(decrypted)));
        } catch {
          resolve(null);
        }
      };
    });
  }
}

Input Validation

// Comprehensive input validation

import { z } from 'zod';

// Define strict schemas
const userRegistrationSchema = z.object({
  email: z.string()
    .email('Invalid email format')
    .max(254, 'Email too long')
    .transform(email => email.toLowerCase().trim()),

  password: z.string()
    .min(12, 'Password must be at least 12 characters')
    .max(128, 'Password too long')
    .regex(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])/,
      'Password must include uppercase, lowercase, number, and special character'
    ),

  username: z.string()
    .min(3, 'Username must be at least 3 characters')
    .max(30, 'Username too long')
    .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens')
    .transform(username => username.toLowerCase()),

  dateOfBirth: z.string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format')
    .refine(date => {
      const birthDate = new Date(date);
      const age = (Date.now() - birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000);
      return age >= 13;
    }, 'Must be at least 13 years old'),
});

// Validate with detailed error handling
export async function validateRegistration(data: unknown) {
  const result = userRegistrationSchema.safeParse(data);

  if (!result.success) {
    const errors = result.error.issues.map(issue => ({
      field: issue.path.join('.'),
      message: issue.message,
    }));

    throw new ValidationError('Validation failed', errors);
  }

  return result.data;
}

// React form with validation
function RegistrationForm() {
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    try {
      const validated = await validateRegistration({
        email: formData.get('email'),
        password: formData.get('password'),
        username: formData.get('username'),
        dateOfBirth: formData.get('dateOfBirth'),
      });

      // Submit validated data
      await submitRegistration(validated);
    } catch (error) {
      if (error instanceof ValidationError) {
        const errorMap: Record<string, string> = {};
        error.errors.forEach(err => {
          errorMap[err.field] = err.message;
        });
        setErrors(errorMap);
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        autoComplete="email"
        // Prevent autofill attacks
        readOnly
        onFocus={(e) => e.target.removeAttribute('readonly')}
      />
      {errors.email && <span className="error">{errors.email}</span>}
      {/* ... other fields */}
    </form>
  );
}

// API input validation middleware
function validateRequest<T extends z.ZodSchema>(schema: T) {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      req.body = await schema.parseAsync(req.body);
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          error: 'Validation failed',
          details: error.issues.map(issue => ({
            field: issue.path.join('.'),
            message: issue.message,
          })),
        });
      }
      next(error);
    }
  };
}

// Usage
app.post(
  '/api/users/register',
  validateRequest(userRegistrationSchema),
  async (req, res) => {
    // req.body is now typed and validated
    const user = await createUser(req.body);
    res.json(user);
  }
);

Part 6: Security Headers

Essential Security Headers

// Comprehensive security headers middleware

interface SecurityHeadersConfig {
  contentSecurityPolicy?: string;
  strictTransportSecurity?: boolean;
  frameOptions?: 'DENY' | 'SAMEORIGIN';
  contentTypeNosniff?: boolean;
  referrerPolicy?: ReferrerPolicy;
  permissionsPolicy?: Record<string, string[]>;
}

type ReferrerPolicy =
  | 'no-referrer'
  | 'no-referrer-when-downgrade'
  | 'origin'
  | 'origin-when-cross-origin'
  | 'same-origin'
  | 'strict-origin'
  | 'strict-origin-when-cross-origin'
  | 'unsafe-url';

const DEFAULT_SECURITY_HEADERS: SecurityHeadersConfig = {
  strictTransportSecurity: true,
  frameOptions: 'DENY',
  contentTypeNosniff: true,
  referrerPolicy: 'strict-origin-when-cross-origin',
  permissionsPolicy: {
    camera: [],
    microphone: [],
    geolocation: ['self'],
    payment: ['self', 'https://payments.example.com'],
  },
};

export function securityHeaders(config: SecurityHeadersConfig = {}) {
  const settings = { ...DEFAULT_SECURITY_HEADERS, ...config };

  return (req: Request, res: Response, next: NextFunction) => {
    // HTTP Strict Transport Security
    // Forces HTTPS for specified duration
    if (settings.strictTransportSecurity) {
      res.setHeader(
        'Strict-Transport-Security',
        'max-age=31536000; includeSubDomains; preload'
      );
    }

    // X-Frame-Options (legacy, use CSP frame-ancestors)
    // Prevents clickjacking
    if (settings.frameOptions) {
      res.setHeader('X-Frame-Options', settings.frameOptions);
    }

    // X-Content-Type-Options
    // Prevents MIME type sniffing
    if (settings.contentTypeNosniff) {
      res.setHeader('X-Content-Type-Options', 'nosniff');
    }

    // Referrer-Policy
    // Controls referrer information sent with requests
    if (settings.referrerPolicy) {
      res.setHeader('Referrer-Policy', settings.referrerPolicy);
    }

    // Permissions-Policy (formerly Feature-Policy)
    // Controls browser features
    if (settings.permissionsPolicy) {
      const policy = Object.entries(settings.permissionsPolicy)
        .map(([feature, origins]) => {
          if (origins.length === 0) {
            return `${feature}=()`;
          }
          const originList = origins.map(o => o === 'self' ? 'self' : `"${o}"`).join(' ');
          return `${feature}=(${originList})`;
        })
        .join(', ');

      res.setHeader('Permissions-Policy', policy);
    }

    // X-XSS-Protection (legacy, mostly disabled)
    // Modern browsers use CSP instead
    res.setHeader('X-XSS-Protection', '0');

    // X-DNS-Prefetch-Control
    // Controls DNS prefetching
    res.setHeader('X-DNS-Prefetch-Control', 'off');

    // X-Download-Options (IE)
    res.setHeader('X-Download-Options', 'noopen');

    // X-Permitted-Cross-Domain-Policies (Flash/PDF)
    res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');

    // Cross-Origin-Opener-Policy
    // Isolates browsing context
    res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');

    // Cross-Origin-Embedder-Policy
    // Required for SharedArrayBuffer
    res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');

    // Cross-Origin-Resource-Policy
    // Prevents other origins from loading this resource
    res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');

    // Cache-Control for sensitive pages
    if (req.path.startsWith('/api/') || req.path.includes('/account')) {
      res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
      res.setHeader('Pragma', 'no-cache');
      res.setHeader('Expires', '0');
    }

    next();
  };
}

// Next.js headers configuration
// next.config.js
const securityHeadersNextConfig = [
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=31536000; includeSubDomains; preload',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=(self)',
  },
  {
    key: 'Cross-Origin-Opener-Policy',
    value: 'same-origin',
  },
  {
    key: 'Cross-Origin-Embedder-Policy',
    value: 'require-corp',
  },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeadersNextConfig,
      },
    ];
  },
};

Part 7: Subresource Integrity (SRI)

Implementing SRI

// Subresource Integrity protects against CDN compromise

import { createHash } from 'crypto';
import { readFileSync } from 'fs';

// Generate SRI hash for a file
export function generateSRIHash(content: string | Buffer, algorithm: 'sha256' | 'sha384' | 'sha512' = 'sha384'): string {
  const hash = createHash(algorithm)
    .update(content)
    .digest('base64');

  return `${algorithm}-${hash}`;
}

// Generate SRI for all static assets during build
interface AssetManifest {
  [path: string]: {
    url: string;
    integrity: string;
  };
}

export function generateAssetManifest(assetDir: string): AssetManifest {
  const manifest: AssetManifest = {};

  // Recursively process all files
  const processDirectory = (dir: string) => {
    const files = readdirSync(dir);

    for (const file of files) {
      const fullPath = join(dir, file);
      const stat = statSync(fullPath);

      if (stat.isDirectory()) {
        processDirectory(fullPath);
      } else if (/\.(js|css)$/.test(file)) {
        const content = readFileSync(fullPath);
        const relativePath = relative(assetDir, fullPath);

        manifest[relativePath] = {
          url: `/static/${relativePath}`,
          integrity: generateSRIHash(content),
        };
      }
    }
  };

  processDirectory(assetDir);
  return manifest;
}

// React component for SRI-protected scripts
interface SRIScriptProps {
  src: string;
  integrity: string;
  async?: boolean;
  defer?: boolean;
}

function SRIScript({ src, integrity, async, defer }: SRIScriptProps) {
  return (
    <script
      src={src}
      integrity={integrity}
      crossOrigin="anonymous" // Required for SRI
      async={async}
      defer={defer}
    />
  );
}

// SRI-protected stylesheet
interface SRIStylesheetProps {
  href: string;
  integrity: string;
}

function SRIStylesheet({ href, integrity }: SRIStylesheetProps) {
  return (
    <link
      rel="stylesheet"
      href={href}
      integrity={integrity}
      crossOrigin="anonymous"
    />
  );
}

// Webpack plugin for automatic SRI
// webpack.config.js
const SriPlugin = require('webpack-subresource-integrity');

module.exports = {
  output: {
    crossOriginLoading: 'anonymous',
  },
  plugins: [
    new SriPlugin({
      hashFuncNames: ['sha384'],
      enabled: process.env.NODE_ENV === 'production',
    }),
  ],
};

// Verify SRI at runtime (for dynamically loaded scripts)
async function loadScriptWithSRI(url: string, expectedHash: string): Promise<void> {
  const response = await fetch(url);
  const content = await response.text();

  // Calculate hash
  const encoder = new TextEncoder();
  const data = encoder.encode(content);
  const hashBuffer = await crypto.subtle.digest('SHA-384', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashBase64 = btoa(String.fromCharCode(...hashArray));
  const actualHash = `sha384-${hashBase64}`;

  if (actualHash !== expectedHash) {
    throw new Error(`SRI hash mismatch for ${url}`);
  }

  // Safe to execute
  const script = document.createElement('script');
  script.textContent = content;
  document.head.appendChild(script);
}

Part 8: Supply Chain Security

NPM Package Security

// Protecting against malicious packages

// 1. Lock file integrity
// Always commit package-lock.json / yarn.lock / pnpm-lock.yaml

// 2. Audit dependencies regularly
// package.json scripts
{
  "scripts": {
    "security:audit": "npm audit --production",
    "security:audit:fix": "npm audit fix",
    "security:check": "npx better-npm-audit audit"
  }
}

// 3. Pre-install hook to verify packages
// .npmrc
//save-exact=true
//ignore-scripts=true  // Disable postinstall scripts (can be malicious)

// 4. Use package.json overrides for vulnerable transitive deps
{
  "overrides": {
    "vulnerable-package": "^2.0.0"
  }
}

// 5. CI/CD security checks
// .github/workflows/security.yml
/*
name: Security Scan
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm audit --audit-level=high
      - name: Snyk Security Scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
*/

// 6. Runtime detection of prototype pollution
function freezePrototypes() {
  Object.freeze(Object.prototype);
  Object.freeze(Array.prototype);
  Object.freeze(Function.prototype);
  Object.freeze(String.prototype);
  Object.freeze(Number.prototype);
  Object.freeze(Boolean.prototype);
}

// 7. Monitor for new vulnerabilities
// package.json
{
  "scripts": {
    "postinstall": "npm audit --json > audit-report.json || true"
  }
}

// 8. Safe package import pattern
// Avoid dynamic requires/imports with user input

// ❌ DANGEROUS
const module = await import(userInput);

// ✅ SAFE: Whitelist allowed modules
const ALLOWED_MODULES = {
  'charts': () => import('./charts'),
  'forms': () => import('./forms'),
  'tables': () => import('./tables'),
};

async function loadModule(name: string) {
  const loader = ALLOWED_MODULES[name];
  if (!loader) {
    throw new Error(`Unknown module: ${name}`);
  }
  return loader();
}

Part 9: Security Monitoring & Logging

Client-Side Security Monitoring

// Comprehensive security event monitoring

interface SecurityEvent {
  type: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
  message: string;
  metadata: Record<string, unknown>;
  timestamp: number;
  url: string;
  userAgent: string;
  sessionId?: string;
}

class SecurityMonitor {
  private events: SecurityEvent[] = [];
  private flushInterval: number = 10000; // 10 seconds
  private maxEvents: number = 100;

  constructor() {
    this.initializeMonitoring();
    this.startPeriodicFlush();
  }

  private initializeMonitoring() {
    // Monitor CSP violations
    document.addEventListener('securitypolicyviolation', (e) => {
      this.logEvent({
        type: 'csp_violation',
        severity: 'high',
        message: `CSP violation: ${e.violatedDirective}`,
        metadata: {
          blockedURI: e.blockedURI,
          violatedDirective: e.violatedDirective,
          originalPolicy: e.originalPolicy,
          sourceFile: e.sourceFile,
          lineNumber: e.lineNumber,
        },
      });
    });

    // Monitor unhandled promise rejections
    window.addEventListener('unhandledrejection', (e) => {
      if (this.isSecurityRelated(e.reason)) {
        this.logEvent({
          type: 'unhandled_security_error',
          severity: 'medium',
          message: e.reason?.message || 'Unknown error',
          metadata: {
            stack: e.reason?.stack,
          },
        });
      }
    });

    // Monitor suspicious DOM modifications
    this.monitorDOMChanges();

    // Monitor suspicious network requests
    this.monitorNetworkRequests();

    // Detect debugging/tampering
    this.detectTampering();
  }

  private monitorDOMChanges() {
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of Array.from(mutation.addedNodes)) {
          if (node instanceof HTMLScriptElement) {
            // Script added dynamically
            if (!node.nonce && node.src) {
              this.logEvent({
                type: 'suspicious_script_injection',
                severity: 'critical',
                message: 'Script added without nonce',
                metadata: {
                  src: node.src,
                  innerHTML: node.innerHTML?.substring(0, 200),
                },
              });
            }
          }

          if (node instanceof HTMLIFrameElement) {
            this.logEvent({
              type: 'iframe_injection',
              severity: 'high',
              message: 'IFrame added dynamically',
              metadata: {
                src: node.src,
              },
            });
          }
        }
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  private monitorNetworkRequests() {
    // Override fetch
    const originalFetch = window.fetch;
    window.fetch = async (input, init) => {
      const url = typeof input === 'string' ? input : input.url;

      // Log requests to unusual domains
      try {
        const parsedUrl = new URL(url, window.location.origin);
        if (this.isSuspiciousDomain(parsedUrl.hostname)) {
          this.logEvent({
            type: 'suspicious_request',
            severity: 'high',
            message: `Request to suspicious domain: ${parsedUrl.hostname}`,
            metadata: { url, method: init?.method || 'GET' },
          });
        }
      } catch {}

      return originalFetch(input, init);
    };

    // Override XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method: string, url: string, ...args: any[]) {
      try {
        const parsedUrl = new URL(url, window.location.origin);
        if (this.isSuspiciousDomain(parsedUrl.hostname)) {
          securityMonitor.logEvent({
            type: 'suspicious_xhr',
            severity: 'high',
            message: `XHR to suspicious domain: ${parsedUrl.hostname}`,
            metadata: { url, method },
          });
        }
      } catch {}

      return originalOpen.call(this, method, url, ...args);
    };
  }

  private detectTampering() {
    // Detect if DevTools is open (basic check)
    const devtools = {
      isOpen: false,
      orientation: undefined as string | undefined,
    };

    const threshold = 160;

    const emitEvent = () => {
      if (!devtools.isOpen) {
        this.logEvent({
          type: 'devtools_opened',
          severity: 'low',
          message: 'Developer tools opened',
          metadata: { orientation: devtools.orientation },
        });
      }
      devtools.isOpen = true;
    };

    setInterval(() => {
      const widthThreshold = window.outerWidth - window.innerWidth > threshold;
      const heightThreshold = window.outerHeight - window.innerHeight > threshold;

      if (widthThreshold || heightThreshold) {
        emitEvent();
      } else {
        devtools.isOpen = false;
      }
    }, 500);
  }

  private isSecurityRelated(error: any): boolean {
    if (!error) return false;
    const securityKeywords = ['csrf', 'xss', 'auth', 'token', 'session', 'forbidden', 'unauthorized'];
    const message = (error.message || '').toLowerCase();
    return securityKeywords.some(keyword => message.includes(keyword));
  }

  private isSuspiciousDomain(hostname: string): boolean {
    // Check against known malicious domains or unexpected domains
    const trustedDomains = [
      'example.com',
      'api.example.com',
      'cdn.example.com',
      'analytics.example.com',
    ];

    return !trustedDomains.some(domain =>
      hostname === domain || hostname.endsWith(`.${domain}`)
    );
  }

  logEvent(event: Omit<SecurityEvent, 'timestamp' | 'url' | 'userAgent'>) {
    const fullEvent: SecurityEvent = {
      ...event,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      sessionId: this.getSessionId(),
    };

    this.events.push(fullEvent);

    // Immediately flush critical events
    if (event.severity === 'critical') {
      this.flush();
    }

    // Prevent memory leak
    if (this.events.length > this.maxEvents) {
      this.flush();
    }
  }

  private flush() {
    if (this.events.length === 0) return;

    const eventsToSend = [...this.events];
    this.events = [];

    // Use sendBeacon for reliable delivery
    navigator.sendBeacon(
      '/api/security/events',
      JSON.stringify(eventsToSend)
    );
  }

  private startPeriodicFlush() {
    setInterval(() => this.flush(), this.flushInterval);
    window.addEventListener('beforeunload', () => this.flush());
  }

  private getSessionId(): string | undefined {
    // Get from your session management
    return undefined;
  }
}

// Initialize global monitor
const securityMonitor = new SecurityMonitor();
export { securityMonitor };

Part 10: Security Checklist

Pre-Production Security Checklist

## Authentication & Sessions
- [ ] Passwords hashed with bcrypt/Argon2 (cost factor ≥ 12)
- [ ] Session tokens are cryptographically random (≥ 128 bits)
- [ ] Sessions expire after inactivity and have absolute timeout
- [ ] Session regeneration on authentication level change
- [ ] Secure cookie attributes: HttpOnly, Secure, SameSite=Strict
- [ ] Multi-factor authentication available for sensitive accounts
- [ ] Account lockout after failed attempts (with notification)
- [ ] Password reset tokens are single-use and expire quickly

## XSS Prevention
- [ ] User input sanitized with DOMPurify before HTML rendering
- [ ] Context-aware output encoding (HTML, JS, URL, CSS)
- [ ] CSP implemented with nonce or hash-based script allowlisting
- [ ] No use of dangerous functions: innerHTML, eval(), document.write()
- [ ] URL validation blocks javascript: and data: protocols
- [ ] JSON data properly escaped when embedded in HTML

## CSRF Protection
- [ ] Anti-CSRF tokens for state-changing operations
- [ ] SameSite=Strict on session cookies
- [ ] Token validation on server-side
- [ ] Tokens are unique per session

## Security Headers
- [ ] Content-Security-Policy (strict, nonce-based)
- [ ] Strict-Transport-Security (HSTS with preload)
- [ ] X-Frame-Options: DENY (or CSP frame-ancestors)
- [ ] X-Content-Type-Options: nosniff
- [ ] Referrer-Policy: strict-origin-when-cross-origin
- [ ] Permissions-Policy restricting dangerous features

## Data Protection
- [ ] HTTPS enforced everywhere (HSTS preload)
- [ ] Sensitive data not stored in localStorage/sessionStorage
- [ ] Client-side encryption for sensitive offline data
- [ ] No secrets in client-side code or logs
- [ ] PII minimized and properly protected

## Input Validation
- [ ] All input validated server-side (client-side is UX only)
- [ ] Strict schema validation (Zod/Joi/Yup)
- [ ] File uploads validated (type, size, content)
- [ ] Redirect URLs validated against whitelist

## Dependencies
- [ ] Regular npm audit with no high/critical issues
- [ ] Lockfile committed and integrity verified
- [ ] Automated vulnerability scanning in CI/CD
- [ ] No packages with known security issues
- [ ] SRI hashes for CDN-loaded scripts

## Logging & Monitoring
- [ ] Security events logged (auth failures, CSRF, CSP violations)
- [ ] No sensitive data in logs (passwords, tokens, PII)
- [ ] Real-time alerting for critical security events
- [ ] Audit trail for sensitive operations

## Error Handling
- [ ] Generic error messages to users (no stack traces)
- [ ] Detailed errors logged server-side only
- [ ] Custom error pages (no framework defaults)

## API Security
- [ ] Rate limiting implemented
- [ ] Request size limits enforced
- [ ] CORS properly configured (not *)
- [ ] GraphQL query depth/complexity limits

Conclusion

Frontend security is not optional—it's a critical engineering discipline. Key takeaways:

  1. Defense in Depth: Never rely on a single security measure. Layer defenses: CSP + input sanitization + output encoding.

  2. Trust No Input: All user input, URL parameters, localStorage data, and even data from your own API should be validated and sanitized.

  3. Secure by Default: Make the secure path the easy path. Use frameworks and libraries that handle security correctly out of the box.

  4. Monitor Everything: You can't protect what you can't see. Implement comprehensive security monitoring and alerting.

  5. Keep Updated: Security is a moving target. Regularly audit dependencies, review new vulnerability disclosures, and update defenses accordingly.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    SECURITY DEFENSE LAYERS                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Layer 1: Network                                                          │
│  ├── HTTPS everywhere (HSTS preload)                                       │
│  ├── Certificate pinning (mobile)                                          │
│  └── Rate limiting                                                         │
│                                                                             │
│  Layer 2: HTTP Headers                                                      │
│  ├── Content-Security-Policy                                               │
│  ├── Strict-Transport-Security                                             │
│  ├── X-Frame-Options                                                       │
│  └── Permissions-Policy                                                    │
│                                                                             │
│  Layer 3: Authentication                                                    │
│  ├── Secure session management                                             │
│  ├── JWT best practices                                                    │
│  ├── CSRF tokens                                                           │
│  └── MFA                                                                   │
│                                                                             │
│  Layer 4: Application                                                       │
│  ├── Input validation                                                      │
│  ├── Output encoding                                                       │
│  ├── HTML sanitization                                                     │
│  └── URL validation                                                        │
│                                                                             │
│  Layer 5: Monitoring                                                        │
│  ├── Security event logging                                                │
│  ├── CSP violation reports                                                 │
│  ├── Anomaly detection                                                     │
│  └── Incident response                                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Security vulnerabilities in production are not just technical failures—they're trust failures. Build security into your frontend from day one.

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.