Skip to content
SP StackPractices
intermediate Por StackPractices

Guía Completa de Web Security Headers

Implementa CSP, HSTS, X-Frame-Options y headers seguros. Cubre content security policy, CORS, referrer policy, permissions policy y testing con security scanners.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Guía Completa de Web Security Headers

Introducción

Los HTTP security headers le dicen al browser cómo comportarse al manejar el contenido de tu sitio. Previene clickjacking, XSS, MIME-type sniffing, downgrade attacks e information leakage. Esta guía cubre cada security header principal, cómo configurarlos y cómo testear que funcionen.

Content-Security-Policy (CSP)

CSP es el security header más poderoso. Restringe qué recursos el browser puede cargar — scripts, styles, images, fonts, frames y connections.

CSP básico

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';

CSP con CDN e inline scripts

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'nonce-abc123'; style-src 'self' https://fonts.googleapis.com 'unsafe-hashes' 'sha256-abc123'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';

CSP con nonces (recomendado para contenido dinámico)

<!-- El server genera un nonce único por request -->
<script nonce="abc123">
  console.log("This inline script is allowed");
</script>
Content-Security-Policy: script-src 'self' 'nonce-abc123'

CSP con hashes (para inline scripts estáticos)

# Generar SHA256 hash del contenido del script
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64 -A
Content-Security-Policy: script-src 'self' 'sha256-abc123='

Report-only mode (testing antes de enforcear)

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

Referencia de directivas CSP

DirectivaControla
default-srcFallback para todos los resource types
script-srcSources de JavaScript
style-srcSources de CSS
img-srcSources de images
font-srcSources de fonts
connect-srcXHR, fetch, WebSocket, EventSource
frame-srcSources de iframe
frame-ancestorsQuién puede embeber esta page (anti-clickjacking)
object-srcFlash, Java, PDF embeds
media-srcSources de audio y video
manifest-srcWeb app manifest
worker-srcWeb Workers
base-uriRestricción del elemento <base>
form-actionTargets de form submission
upgrade-insecure-requestsAuto-upgrade HTTP a HTTPS

Strict-Transport-Security (HSTS)

Fuerza al browser a usar HTTPS para todos los futuros requests a este dominio.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age=31536000 — 1 año en segundos
  • includeSubDomains — aplica a todos los subdominios
  • preload — opt-in a las HSTS preload lists de los browsers

HSTS preload list

Submitir tu dominio en hstspreload.org para ser incluido en la preload list bundled de Chrome. Requisitos:

  • Certificado HTTPS válido
  • Redirect HTTP a HTTPS en el mismo dominio
  • HSTS header con max-age >= 31536000, includeSubDomains y preload
  • Todos los subdominios sirven HTTPS

X-Frame-Options

Previene clickjacking controlando quién puede embeber tu page en un iframe.

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

Nota: frame-ancestors en CSP reemplaza este header. Usar CSP frame-ancestors para browsers modernos, pero mantener X-Frame-Options para soporte legacy.

X-Content-Type-Options

Previene MIME-type sniffing — el browser respeta el Content-Type declarado.

X-Content-Type-Options: nosniff

Referrer-Policy

Controla cuánta referrer information se envía con los requests.

Referrer-Policy: strict-origin-when-cross-origin
ValorReferrer enviado
no-referrerNinguno
no-referrer-when-downgradeFull URL en HTTPS→HTTPS, nada en HTTPS→HTTP
same-originFull URL solo para same-origin requests
originSolo origin (sin path)
strict-originSolo origin, nada en downgrade
origin-when-cross-originFull URL same-origin, solo origin cross-origin
strict-origin-when-cross-originFull URL same-origin, solo origin cross-origin, nada en downgrade
unsafe-urlFull URL siempre (no recomendado)

Permissions-Policy

Controla qué features y APIs del browser la page puede usar (anteriormente Feature-Policy).

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self "https://trusted.com"), usb=()

Features comunes

Permissions-Policy: accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=()

Cross-Origin Resource Sharing (CORS)

CORS no es un solo header sino un set de headers que controlan cross-origin requests.

CORS simple

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

CORS con credentials

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Authorization

Nota: No puedes usar Access-Control-Allow-Origin: * con Access-Control-Allow-Credentials: true. Debes especificar el origin exacto.

Preflight requests

# El browser envía OPTIONS request primero
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Cross-Origin Opener Policy (COOP)

Aísla tu page de otros origins para prevenir Spectre-style attacks.

Cross-Origin-Opener-Policy: same-origin

Cross-Origin Embedder Policy (COEP)

Controla qué cross-origin resources pueden ser cargados.

Cross-Origin-Embedder-Policy: require-corp

Cross-Origin Resource Policy (CORP)

Restringe quién puede embeber un resource.

Cross-Origin-Resource-Policy: same-origin

Configuración de Server

Nginx

server {
    listen 443 ssl http2;
    server_name example.com;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
}

Apache

<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    Header always set X-Frame-Options "DENY"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
</IfModule>

Express.js

const helmet = require("helmet");

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
      styleSrc: ["'self'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
    },
  },
  strictTransportSecurity: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

Testing

Scanners online

Browser DevTools

# Chrome DevTools → Security tab
# Muestra: TLS connection, security headers, insecure content warnings

# Chrome DevTools → Network tab → Response Headers
# Inspeccionar cada header en cualquier response

CSP violation reporting

// Server endpoint para recibir CSP violation reports
app.post("/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
  console.log("CSP violation:", req.body);
  res.status(204).end();
});

Pautas

  • Empezar con CSP report-only — identificar violations antes de enforcear
  • Usar nonces sobre hashes para contenido dinámico — hashes rompen cuando el contenido cambia
  • Setear frame-ancestors 'none' — protección más fuerte contra clickjacking
  • Siempre incluir upgrade-insecure-requests — auto-upgrade HTTP resources
  • Usar strict-origin-when-cross-origin referrer policy — buen default de privacy
  • Preload HSTS — proteger contra downgrade attacks en primera visita
  • Testear con securityheaders.com — apuntar a rating A+
  • Setear headers en todas las responses — usar always en Nginx/Apache para incluir error responses
  • Restringir Permissions-Policy agresivamente — deshabilitar features que no usas
  • Usar Helmet para Node.js — defaults sensatos con customización fácil
  • Reviewar CSP mensualmente — nuevos third-party scripts pueden romper bajo strict CSP
  • Separar CORS por ruta — no setear global Access-Control-Allow-Origin: *

Errores Comunes

  • Usar unsafe-inline en CSP — derrota la protección XSS enteramente
  • Setear Access-Control-Allow-Origin: * con credentials — los browsers rechazan esto
  • No incluir always en Nginx add_header — las error pages pierden security headers
  • Usar HSTS preload sin testear — no se puede deshacer fácilmente (toma meses)
  • Setear X-Frame-Options: ALLOW-FROM — deprecado, usar CSP frame-ancestors en su lugar
  • No testear CSP antes de enforcear — rompe pages de producción silenciosamente
  • Olvidar object-src 'none' — permite Flash/Java embeds
  • No setear headers en API responses — las APIs necesitan security headers también
  • Usar unsafe-eval en CSP — requerido por algunos frameworks pero debilita security
  • No monitorear CSP reports — las violations pasan desapercibidas en producción

Preguntas Frecuentes

¿Cuál es la diferencia entre CSP frame-ancestors y X-Frame-Options?

frame-ancestors en CSP es el reemplazo moderno de X-Frame-Options. Soporta múltiples origins y wildcards, mientras que X-Frame-Options solo soporta DENY o SAMEORIGIN. Mantener ambos para soporte legacy, pero usar CSP como control primario.

¿Cómo debuggeo CSP violations?

Chequear la console del browser — las CSP violations se loguean con la URL bloqueada y la directiva que la bloqueó. Usar Content-Security-Policy-Report-Only para colectar violations sin romper la page. Setear un reporting endpoint para agregar violations en producción.

¿Debo usar strict-dynamic en CSP?

strict-dynamic permite que scripts cargados por scripts de confianza (nonces o hashes) carguen otros scripts. Esto reduce la necesidad de mantener una whitelist de script URLs. Es recomendado para aplicaciones con dynamic script loading, pero requiere soporte de CSP Level 3 (Chrome 52+, Firefox 52+, Safari 15.4+).