Proteger APIs con encabezados de seguridad HTTP
Cómo configurar headers de seguridad esenciales como HSTS, CSP y X-Frame-Options para proteger APIs y aplicaciones web de ataques comunes.
Visión general
Los encabezados de seguridad HTTP son una capa de defensa ligera del lado del servidor que instruye a los navegadores cómo manejar tu contenido. No requieren cambios en el código de aplicación y protegen contra clases enteras de ataques: clickjacking vía X-Frame-Options, cross-site scripting vía Content-Security-Policy, ataques de downgrade de protocolo vía Strict-Transport-Security, y sniffing de MIME-type vía X-Content-Type-Options.
El OWASP Secure Headers Project mantiene una referencia dedicada para estos headers porque son útiles, fáciles de implementar y se olvidan con frecuencia en los despliegues. Un servidor sin estos headers no es inmediatamente vulnerable, pero es considerablemente menos resistente a los ataques web habituales. Para un endurecimiento más amplio de la aplicación, consulta la guía de buenas prácticas de seguridad.
Cuándo usarlo
Usa esta receta cuando:
- Lanzas una nueva aplicación web o API a producción
- Realizas auditorías de seguridad o pruebas de penetración
- Endureces aplicaciones existentes después de una revisión de seguridad
- Configuras proxies inversos (Nginx, Apache, CloudFront, Cloudflare)
- Construyes middleware para aplicaciones Express, FastAPI o Spring Boot
Omítelos si todos tus consumidores son APIs máquina a máquina o apps nativas — los headers son instrucciones del navegador y un cliente no-web los ignora.
Solución
Express.js Middleware
const helmet = require('helmet');
const express = require('express');
const app = express();
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
strictTransportSecurity: {
maxAge: 63072000, // 2 años, según recomendación OWASP
includeSubDomains: true,
},
}));
// Los defaults de Helmet ya configuran COOP, COEP, CORP,
// Origin-Agent-Cluster y X-Content-Type-Options; solo
// personalizas lo que difiere.
Configuración Nginx
server {
listen 443 ssl;
server_name api.example.com;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" 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 "geolocation=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
}
FastAPI (Python)
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
app = FastAPI()
app.add_middleware(SecurityHeadersMiddleware)
Explicación
- Strict-Transport-Security (HSTS): Indica a los navegadores que siempre usen HTTPS para tu dominio. Previene ataques de SSL stripping donde un man-in-the-middle degrada la conexión a HTTP. Deja
preloadfuera de la configuración hasta que todos los subdominios sirvan HTTPS; una vez en la lista de preload, la baja es lenta. - Content-Security-Policy (CSP): Restringe desde dónde pueden cargarse scripts, estilos, imágenes y otros recursos. Un CSP estricto bloquea scripts inline y dominios externos no autorizados, neutralizando XSS aunque un atacante inyecte marcado. Para una API que solo sirve JSON,
default-src 'none'es suficiente. - X-Frame-Options: Impide que tu sitio se incruste en un
<iframe>en otro dominio. Bloquea ataques de clickjacking donde el atacante superpone iframes invisibles para engañar al usuario. La directivaframe-ancestorsde CSP cubre lo mismo con más control. - X-Content-Type-Options: Configurar
nosniffevita que los navegadores interpreten archivos como un tipo MIME distinto al declarado. Mitiga ataques donde un.txtsubido por un usuario se ejecuta como JavaScript. - Referrer-Policy: Controla cuánta URL se comparte en el header
Refereren peticiones salientes.strict-origin-when-cross-originenvía la URL completa solo en peticiones del mismo origen. - Permissions-Policy: Desactiva funciones del navegador que no necesitas (cámara, micrófono, geolocalización, pagos). Una lista vacía por función es el endurecimiento más barato que existe.
- Cross-Origin-Opener/Resource/Embedder-Policy: El trío COOP/COEP/CORP aísla tu contexto de navegación y declara qué orígenes pueden incrustar tus recursos, bloqueando fugas cross-origin tipo Spectre.
- Cache-Control: No es un “security header” en sentido estricto, pero
no-storeen respuestas que llevan tokens o datos personales los mantiene fuera de cachés compartidas y del navegador.
Así viaja un header desde tu servidor hasta el motor de políticas del navegador:
Variantes
| Header | Ataque prevenido | Requerido? | Soporte de navegador |
|---|---|---|---|
| HSTS | SSL stripping | Sí | Universal |
| CSP | XSS, inyección de datos | Sí | Universal |
| X-Frame-Options | Clickjacking | Sí | Universal |
| X-Content-Type-Options | MIME sniffing | Sí | Universal |
| Referrer-Policy | Fuga de información | Recomendado | Universal |
| Permissions-Policy | Abuso de funcionalidades | Recomendado | Moderno |
| Cross-Origin-Opener-Policy | Fugas de contexto (Spectre) | Recomendado | Moderno |
| Cross-Origin-Resource-Policy | Incrustación no autorizada | Recomendado | Moderno |
| Clear-Site-Data | Datos de sesión obsoletos | Respuestas de logout | Moderno |
| Cache-Control: no-store | Datos sensibles en caché | Sí, para APIs | Universal |
Evita los deprecados: X-XSS-Protection (eliminado de los navegadores, habilitaba vulnerabilidades) y Feature-Policy (el nombre antiguo de Permissions-Policy).
Lo que funciona
- Usa Helmet como base: el middleware Helmet para Express configura defaults sensatos para todos los headers principales con una sola línea de código.
- Empieza con un CSP restrictivo y relaja gradualmente: comienza con
default-src 'self'y agrega dominios solo cuando la funcionalidad se rompe. Un CSP demasiado permisivo es casi inútil. - Trata el preload de HSTS como decisión aparte: cuando todos los subdominios sirvan HTTPS, añade la directiva
preloady envía el dominio a hstspreload.org. No lo actives por defecto: la baja es lenta y rompe cualquier subdominio que aún necesite HTTP. - Incluye headers en todas las respuestas: las páginas de error (404, 500) y las respuestas de API deben llevar los mismos headers que las páginas HTML. Los atacantes también apuntan a las páginas de error.
- Prueba con securityheaders.com o Mozilla Observatory: estas herramientas escanean tu sitio y califican tu configuración con pasos concretos de remediación.
Errores comunes
- Usar
ALLOW-FROMen X-Frame-Options: los navegadores modernos no soportan este valor. UsaSAMEORIGINoDENYen su lugar. - Habilitar
unsafe-inlinepara scripts en CSP: esto desactiva la protección XSS de CSP. Usa nonces o hashes si los scripts inline son inevitables. - Olvidar endpoints de API: los headers de seguridad a menudo se configuran para rutas HTML pero se omiten de las respuestas JSON de la API. Aplícalos globalmente.
- Configurar HSTS sin HTTPS listo: si tu sitio todavía sirve tráfico HTTP, HSTS lo romperá para usuarios que hayan visitado la versión HTTPS antes.
Soluciones Avanzadas
Configuración de encabezados de seguridad en Spring Boot
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.web.SecurityFilterChain;
@Configuration
public class SecurityHeadersConfig {
@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(63072000)
.includeSubDomains(true)
)
.contentTypeOptions(opts -> {})
.frameOptions(frame -> frame.deny())
.referrerPolicy(referrer -> referrer.policy(
org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN
))
.permissionsPolicy(permissions -> permissions.policy(
"geolocation=(), microphone=(), camera=(), payment=()"
))
);
return http.build();
}
}
Inyección de encabezados con Cloudflare Workers
Inyecta security headers en el edge sin tocar la infraestructura de origen:
export default {
async fetch(request, env) {
const response = await fetch(request);
// Clonar la respuesta para poder modificar headers
const newResponse = new Response(response.body, response);
// Security headers
newResponse.headers.set('Strict-Transport-Security',
'max-age=63072000; includeSubDomains');
newResponse.headers.set('X-Content-Type-Options', 'nosniff');
newResponse.headers.set('X-Frame-Options', 'DENY');
newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
newResponse.headers.set('Permissions-Policy',
'geolocation=(), microphone=(), camera=()');
// Setear CSP solo para respuestas HTML
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('text/html')) {
newResponse.headers.set('Content-Security-Policy',
"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");
}
return newResponse;
},
};
CORS preflight con encabezados de seguridad
Combina CORS y security headers para APIs cross-origin:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
// Security headers primero
app.use(helmet());
// CORS con allowlist de orígenes específicos
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('No permitido por CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Request-ID', 'X-RateLimit-Remaining'],
credentials: true,
maxAge: 86400, // Cachear preflight por 24 horas
};
app.use(cors(corsOptions));
// Handler explícito de preflight
app.options('*', cors(corsOptions));
// Rutas de API
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
Testing automatizado de encabezados con curl
#!/bin/bash
# audit-headers.sh — Verificar security headers en una URL
URL="${1:-https://example.com}"
REQUIRED_HEADERS=(
"strict-transport-security"
"content-security-policy"
"x-content-type-options"
"x-frame-options"
"referrer-policy"
)
echo "Auditando: $URL"
echo "-----------------------------------"
HEADERS=$(curl -sI "$URL")
for header in "${REQUIRED_HEADERS[@]}"; do
VALUE=$(echo "$HEADERS" | grep -i "^$header:" | sed 's/^[^:]*: *//')
if [ -z "$VALUE" ]; then
echo "FALTA: $header"
else
echo "OK: $header = $VALUE"
fi
done
# Verificar CSP débil
CSP=$(echo "$HEADERS" | grep -i "^content-security-policy:" | sed 's/^[^:]*: *//')
if echo "$CSP" | grep -q "unsafe-inline"; then
echo "ADVERTENCIA: CSP contiene 'unsafe-inline'"
fi
if echo "$CSP" | grep -q "unsafe-eval"; then
echo "ADVERTENCIA: CSP contiene 'unsafe-eval'"
fi
# Verificar HSTS max-age
HSTS=$(echo "$HEADERS" | grep -i "^strict-transport-security:" | sed 's/^[^:]*: *//')
if echo "$HSTS" | grep -q "max-age=0"; then
echo "ADVERTENCIA: HSTS max-age es 0 (deshabilitado)"
fi
Ver también
- Recursos complementarios:
audit-headers.sh, un script Bash ejecutable que audita los headers faltantes y el CSP débil de una URL. - OWASP Secure Headers Project: la lista de referencia de headers y valores recomendados, mantenida al día.
- Headers HTTP en MDN: sintaxis por header y soporte de navegadores.
- hstspreload.org: comprueba elegibilidad y envía tu dominio a la lista de preload.
Preguntas frecuentes
¿Los security headers protegen APIs consumidas por apps móviles?
La mayoría de los security headers son específicos de navegadores. Las apps nativas móviles que usan clientes HTTP no se ven afectadas por CSP o X-Frame-Options. Enfócate en autenticación, validación de input y TLS para comunicación API-a-app.
¿Puedo configurar security headers en un CDN como Cloudflare?
Sí. Cloudflare Transform Rules y AWS CloudFront Functions pueden inyectar headers en el edge sin tocar código de origen. Esto es útil para sitios estáticos o sistemas legacy.
¿Cuál es la diferencia entre CSP y CORS?
CSP controla qué recursos puede cargar un navegador cuando renderiza tu página. CORS controla si otros orígenes pueden hacer peticiones a tu API. Son complementarios, no sustitutos.
¿Debería usar report-uri en CSP?
Sigue funcionando en la mayoría de navegadores, pero está deprecado en favor de report-to y la Reporting API. Combina cualquiera de las dos con Content-Security-Policy-Report-Only durante el despliegue para recolectar violaciones sin bloquear nada, y luego pasa a la directiva estricta.
¿Necesito CSP en una API que solo sirve JSON?
Una mínima, sí. default-src 'none' más frame-ancestors 'none' no cuesta nada y elimina la carga de recursos y el framing (carga de recursos, framing) en respuestas que de todos modos no usan funciones del navegador.
¿Los headers se ponen en el proxy o en la aplicación?
Preferiblemente en el borde. Configurarlos en Nginx, un CDN o un API gateway cubre todas las respuestas de forma uniforme, incluidas las páginas de error que tu app quizá no controla. El middleware a nivel de aplicación queda como respaldo para headers que dependen del contexto de la ruta, como los nonces de CSP.
Recursos Relacionados
Prevenir Cross-Site Scripting (XSS)
Cómo sanitizar input de usuario, escapar output y usar Content Security Policy para prevenir ataques XSS en aplicaciones web.
RecipePrevenir Ataques de Inyección SQL
Cómo escribir queries parametrizadas y usar ORMs para eliminar vulnerabilidades de inyección SQL en Python, JavaScript y Java.
RecipeConfigurar CORS en Express, Flask y Spring Boot
Cómo configurar los encabezados de Cross-Origin Resource Sharing (CORS) correctamente para APIs, SPAs y funciones serverless sin abrir agujeros de seguridad.
RecipeRate Limiting contra Abuso y Ataques de Fuerza Bruta
Rate limiting como control de seguridad contra fuerza bruta, credential stuffing, scraping y DDoS L7: límites por capas, ajuste por endpoint y gestión del 429.
RecipeFirma HMAC para autenticación de API (AWS Sig v4)
Asegurá peticiones de API con firmas HMAC y autenticación AWS Signature v4 para integridad de mensajes a prueba de manipulaciones.
RecipeConfigura headers de seguridad HTTP con Helmet en Node.js
Establece headers de seguridad HTTP en apps Express con Helmet: CSP, HSTS, X-Frame-Options, X-Content-Type-Options y CORS para seguridad web OWASP