StackPractices
beginner Por Mathias Paulenko

Cabeceras de seguridad HTTP

Fortalece aplicaciones web con HTTP security headers: CSP, HSTS, X-Frame-Options y una lista de verificación completa de headers de seguridad.

Temas: security

Visión General

Los HTTP security headers son la primera línea de defensa para aplicaciones web. Instruyen a los navegadores sobre cómo comportarse al renderizar tu sitio — bloqueando XSS, previniendo clickjacking, forzando HTTPS y controlando qué recursos externos pueden cargarse. Configurados correctamente, pueden detener clases enteras de ataques sin cambiar una línea de código de aplicación.

Cuándo Usar

Usa este recurso cuando:

  • Fortaleces aplicaciones web en producción contra ataques comunes basados en navegador
  • Te preparas para auditorías de seguridad que verifican headers seguros de OWASP
  • Sirves contenido generado por usuarios que podría contener scripts maliciosos
  • Embebes tu aplicación en sitios de terceros vía iframes

Solución

Express.js Security Headers Middleware

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'", "https://analytics.example.com"],
      styleSrc: ["'self'", "https://fonts.googleapis.com"],
      imgSrc: ["'self'", "data:", "https://cdn.example.com"],
      connectSrc: ["'self'", "https://api.example.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      frameAncestors: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

Nginx Security Headers

server {
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    location / {
        proxy_pass http://backend;
    }
}

Security Header Audit Script (Bash)

#!/bin/bash
URL="https://example.com"

echo "Checking security headers for $URL..."
curl -sI "$URL" | grep -iE \
  "(content-security-policy|strict-transport-security|x-content-type-options|x-frame-options|referrer-policy|permissions-policy)"

Explicación

Headers esenciales:

HeaderPropósitoRiesgo si Falta
Content-Security-Policy (CSP)Controla scripts/styles cargadosXSS vía tags <script> inyectados
Strict-Transport-Security (HSTS)Fuerza HTTPSAtaques de SSL stripping
X-Content-Type-OptionsPreviene MIME sniffingDrive-by downloads vía content types falsos
X-Frame-OptionsBloquea clickjackingAtaques de UI redressing
Referrer-PolicyControla filtración de referrerURLs sensibles expuestas a terceros
Permissions-PolicyRestringe APIs del navegadorAcceso no autorizado a cámara, micrófono, geolocalización

Variantes

FrameworkLibreríaFacilidad
ExpressHelmetUna línea
Fastify@fastify/helmetPlugin
Djangodjango-cspMiddleware
SpringSpring SecurityConfiguración
Railssecure_headersGem
NginxNativoManual

Lo que funciona

  • Empieza con CSP report-only: Content-Security-Policy-Report-Only logea violaciones sin bloquear
  • Usa nonce para scripts inline: <script nonce="random-value"> en lugar de 'unsafe-inline'
  • Testea antes de enforcear: CSP puede romper widgets de terceros, formas de pago y analytics
  • Envía a lista de preload HSTS: Incluye tu dominio en listas de preload de browsers para HTTPS automático
  • Configura frame ancestors apropiadamente: Usa CSP frame-ancestors en lugar del legacy X-Frame-Options

Errores Comunes

  1. CSP demasiado permisivo: default-src * permite cualquier dominio ejecutar scripts
  2. Faltando en responses de API: Los browsers ignoran headers en páginas 404/500, pero los atacantes las usan
  3. Olvidando endpoints de API: Las APIs JSON deberían enviar CORS y CSP headers
  4. Violaciones sin reporte: Sin report-uri, no sabrás cuando contenido legítimo está bloqueado
  5. Headers inconsistentes: Proxy Nginx sobreescribiendo headers de aplicación crea gaps

Soluciones Avanzadas

CSP report-only con reporte de violaciones

Despliega CSP en modo report-only primero para capturar violaciones sin romper nada:

const express = require('express');
const helmet = require('helmet');

const app = express();

// CSP report-only: logea violaciones pero no bloquea
app.use(helmet.contentSecurityPolicy({
  useDefaults: false,
  directives: {
    'default-src': ["'self'"],
    'script-src': ["'self'", 'https://cdn.example.com'],
    'style-src': ["'self'", 'https://fonts.googleapis.com'],
    'img-src': ["'self'", 'data:', 'https:'],
    'connect-src': ["'self'", 'https://api.example.com'],
    'report-uri': ['/api/csp-report'],
  },
  reportOnly: true,
}));

// Endpoint para recibir reportes de violaciones CSP
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  console.warn('Violación CSP:', {
    'document-uri': report['document-uri'],
    'violated-directive': report['violated-directive'],
    'blocked-uri': report['blocked-uri'],
    'line-number': report['line-number'],
    'source-file': report['source-file'],
  });
  res.status(204).end();
});

// Después de recolectar reportes por 1-2 semanas, cambiar a CSP enforceante
// removiendo reportOnly: true y manteniendo la directiva report-uri

Django security headers middleware

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    # ... otro middleware
]

SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'

# CSP via django-csp
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", 'https://cdn.example.com')
CSP_STYLE_SRC = ("'self'", 'https://fonts.googleapis.com')
CSP_IMG_SRC = ("'self'", 'data:', 'https:')
CSP_CONNECT_SRC = ("'self'", 'https://api.example.com')
CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com')
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_REPORT_URI = '/csp-report/'

Spring Boot security headers

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp.policyDirectives(
                    "default-src 'self'; " +
                    "script-src 'self' https://cdn.example.com; " +
                    "style-src 'self' https://fonts.googleapis.com; " +
                    "img-src 'self' data: https:; " +
                    "connect-src 'self' https://api.example.com; " +
                    "frame-ancestors 'none'; " +
                    "base-uri 'self'; " +
                    "object-src 'none'"
                ))
                .httpStrictTransportSecurity(hsts -> hsts
                    .maxAgeInSeconds(31536000)
                    .includeSubDomains(true)
                    .preload(true)
                )
                .contentTypeOptions(opts -> {}) // X-Content-Type-Options: nosniff
                .frameOptions(frame -> frame.deny())
                .xssProtection(xss -> xss.headerValue(
                    XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK
                ))
                .referrerPolicy(referrer -> referrer.policy(
                    org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN
                ))
            )
            .csrf(csrf -> csrf.disable()); // Deshabilitar CSRF para apps API-only

        return http.build();
    }
}

Headers de aislamiento cross-origin (COEP, COOP, CORP)

Para aplicaciones que necesitan SharedArrayBuffer u otras APIs avanzadas, configura headers de aislamiento cross-origin:

# Nginx: habilitar aislamiento cross-origin
server {
    # Cross-Origin Embedder Policy: requiere CORS para recursos cross-origin
    add_header Cross-Origin-Embedder-Policy "require-corp" always;

    # Cross-Origin Opener Policy: aislar grupo de contexto de navegación
    add_header Cross-Origin-Opener-Policy "same-origin" always;

    # Cross-Origin Resource Policy: restringir quién puede embeber este recurso
    add_header Cross-Origin-Resource-Policy "same-origin" always;

    # Headers de seguridad estándar
    add_header Content-Security-Policy "default-src 'self'" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always;
}

Auditoría automatizada de security headers (Python)

import requests
from typing import dict

REQUIRED_HEADERS = {
    'content-security-policy': 'Bloquea XSS e inyección de recursos',
    'strict-transport-security': 'Fuerza HTTPS',
    'x-content-type-options': 'Previene MIME sniffing',
    'x-frame-options': 'Previene clickjacking',
    'referrer-policy': 'Controla filtración de referrer',
    'permissions-policy': 'Restringe APIs del navegador',
}

def audit_security_headers(url: str) -> dict:
    """Auditar una URL para headers de seguridad faltantes."""
    try:
        response = requests.head(url, allow_redirects=True, timeout=10)
    except requests.RequestException as e:
        return {'error': str(e)}

    results = {
        'url': url,
        'status': response.status_code,
        'present': {},
        'missing': {},
        'warnings': [],
    }

    for header, purpose in REQUIRED_HEADERS.items():
        value = response.headers.get(header)
        if value:
            results['present'][header] = value
            # Verificar configuraciones débiles
            if header == 'content-security-policy' and "'unsafe-inline'" in value:
                results['warnings'].append(
                    f"CSP contiene 'unsafe-inline' — considera usar nonces"
                )
            if header == 'strict-transport-security' and 'max-age=0' in value:
                results['warnings'].append(
                    "HSTS max-age es 0 — HSTS está efectivamente deshabilitado"
                )
        else:
            results['missing'][header] = purpose

    return results

# Uso
audit = audit_security_headers('https://example.com')
print(f"Presentes: {len(audit['present'])}/{len(REQUIRED_HEADERS)}")
if audit['missing']:
    print("Headers faltantes:")
    for h, p in audit['missing'].items():
        print(f"  - {h}: {p}")
if audit['warnings']:
    print("Advertencias:")
    for w in audit['warnings']:
        print(f"  - {w}")

Preguntas frecuentes

¿Esta solución está lista para producción?

Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.

¿Cuáles son las características de rendimiento?

El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.

¿Cómo depuro problemas con este enfoque?

Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.