StackPractices
intermediate Por Mathias Paulenko

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.

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. A continuación: cada security header principal, cómo configurarlos y cómo testear que funcionen.

Content-Security-Policy (CSP)

CSP es el security header más efectivo. 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

See Also

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+).