XSS prevention

All user string inputs are escaped server-side before use. CSP headers block unauthorized script execution.

What is XSS?

Cross-site scripting (XSS) occurs when an attacker injects malicious JavaScript into a web page that executes in another user's browser. In a typical attack, user-supplied input (a name, a comment, a URL) is stored and later rendered as HTML - and the injected script runs with full access to the page.

escapeHTML()

The primary XSS defense in NextForge is escapeHTML() in lib/sanitize.ts - a server-safe function (no DOM APIs) that converts special HTML characters to their entity equivalents:

lib/sanitize.ts
export function escapeHTML(str: string): string {
  return str
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
}

This ensures that a value like <script>alert(1)</script> is stored and rendered as &lt;script&gt;alert(1)&lt;/script&gt; - harmless text rather than executable code.

Sanitize helpers

Three higher-level helpers build on escapeHTML():

lib/sanitize.ts
// Sanitize a single value - type-checks, trims, escapes
export function sanitizeString(str: unknown): string {
  if (typeof str !== 'string') return ''
  return escapeHTML(str.trim())
}

// Recursively sanitize all string values in a plain object
export function sanitizeObject<T extends Record<string, unknown>>(obj: T): T {
  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => [
      key,
      typeof value === 'string' ? sanitizeString(value) :
      typeof value === 'object' && value !== null ?
        sanitizeObject(value as Record<string, unknown>) : value
    ])
  ) as T
}

// Run stripMongoOperators then sanitizeObject
export function sanitizeAndStrip<T extends Record<string, unknown>>(obj: T): T {
  return sanitizeObject(stripMongoOperators(obj) as T)
}

CSP headers

A Content Security Policy header is applied to every response in proxy.ts, providing browser-level protection even if a value escapes server-side sanitization:

proxy.ts
headers.set(
  'Content-Security-Policy',
  "default-src 'self'; " +
  "script-src 'self' 'unsafe-inline'; " +
  "style-src 'self' 'unsafe-inline'; " +
  "img-src 'self' data: https:; " +
  "font-src 'self' data:"
)

Usage in server actions

Every server action calls sanitizeAndStrip() as the very first step, before Zod validation and before any DB operation:

actions/auth.ts
export async function loginAction(formData: FormData): Promise<ActionState> {
  const raw = {
    email: formData.get('email'),
    password: formData.get('password'),
  }

  // Step 1: Sanitize - strips operators, escapes HTML
  const sanitized = sanitizeAndStrip(raw)

  // Step 2: Validate - Zod schema check
  const parsed = parseSchema(loginSchema, sanitized)
  if (!parsed.success) return parsed

  // Step 3: Rate limit
  // Step 4: DB operation
  // ...
}