La seguridad en aplicaciones web no requiere ser un experto en ciberseguridad para implementar correctamente lo básico. La mayoría de los ataques comunes se previenen con tres cosas bien configuradas: HTTPS, CORS y los headers HTTP adecuados. Esta guía es práctica y directa al grano.
HTTPS: no es opcional
En 2026, servir cualquier aplicación sobre HTTP puro es inaceptable. HTTPS cifra la comunicación entre el cliente y el servidor, impidiendo que terceros intercepten o modifiquen los datos en tránsito.
Cómo obtener un certificado gratis con Let’s Encrypt
Si usas Nginx o Apache directamente en un VPS, Certbot automatiza la obtención y renovación:
# Ubuntu / Debian con Nginx
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d tudominio.com -d www.tudominio.com
# Renovación automática (ya la configura certbot, pero puedes verificar)
sudo systemctl status certbot.timer
Si usas Docker + Nginx, la opción más sencilla es Traefik como proxy reverso — detecta servicios automáticamente y gestiona certificados via Let’s Encrypt:
# docker-compose.yml simplificado con Traefik
services:
traefik:
image: traefik:v3
command:
- "--certificatesresolvers.myresolver.acme.email=tu@email.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "letsencrypt:/letsencrypt"
mi-app:
image: mi-app:latest
labels:
- "traefik.http.routers.mi-app.rule=Host(`tudominio.com`)"
- "traefik.http.routers.mi-app.tls.certresolver=myresolver"
Redirigir HTTP a HTTPS siempre
En Nginx:
server {
listen 80;
server_name tudominio.com;
return 301 https://$host$request_uri;
}
CORS: la fuente de confusión número uno
CORS (Cross-Origin Resource Sharing) es el mecanismo que controla qué orígenes externos pueden hacer peticiones a tu API. El error más común es configurarlo demasiado permisivo (Access-Control-Allow-Origin: *) en producción, o configurarlo mal y bloquear tu propio frontend.
Cómo funciona en 60 segundos
Tu frontend en https://app.tudominio.com hace una petición fetch a https://api.tudominio.com. El navegador envía el header Origin: https://app.tudominio.com. Tu API debe responder con Access-Control-Allow-Origin: https://app.tudominio.com para que el navegador permita la respuesta.
Para requests con credenciales (cookies, Authorization headers), debes:
- Especificar el origen exacto (no
*) - Agregar
Access-Control-Allow-Credentials: true - En el frontend, usar
credentials: 'include'
Configuración correcta en Node.js con Express
import cors from 'cors';
const allowedOrigins = [
'https://app.tudominio.com',
'https://tudominio.com',
// Solo en desarrollo:
...(process.env.NODE_ENV === 'development' ? ['http://localhost:5173'] : []),
];
app.use(cors({
origin: (origin, callback) => {
// Permitir requests sin origin (Postman, curl, server-to-server)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origen no permitido: ${origin}`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
El error de preflight
Las peticiones con métodos distintos a GET/POST o con headers personalizados (como Authorization) primero disparan una petición OPTIONS (preflight). Tu servidor debe responder a esa petición con los headers CORS correctos y status 200/204, o el navegador bloqueará la petición real:
// Express ya lo maneja si configuras cors() antes de las rutas
// Pero si usas Nginx como proxy, agrega esto:
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Content-Length 0;
return 204;
}
proxy_pass http://backend:3000/;
}
Headers de seguridad HTTP
Estos headers van en cada respuesta del servidor e instruyen al navegador a comportarse de forma más segura. Son una de las medidas de más impacto por menos esfuerzo.
Los que debes agregar hoy mismo
Strict-Transport-Security (HSTS) — fuerza HTTPS en futuros requests:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Una vez que el navegador lo recibe, no intentará conectar por HTTP durante un año.
X-Content-Type-Options — evita que el navegador adivine el tipo MIME:
X-Content-Type-Options: nosniff
Previene ataques donde un archivo JS malicioso se sirve como imagen.
X-Frame-Options — previene clickjacking:
X-Frame-Options: DENY
Impide que tu sitio sea embebido en un <iframe> de otro dominio.
Referrer-Policy — controla qué información de URL se comparte:
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy — deshabilita APIs del navegador que no usas:
Permissions-Policy: geolocation=(), microphone=(), camera=()
Configuración en Nginx
server {
# ... resto de la config
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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=()" always;
}
Configuración en Express (Node.js)
La librería Helmet agrega automáticamente todos estos headers y más:
npm install helmet
import helmet from 'helmet';
app.use(helmet({
// Ajusta según tu caso
contentSecurityPolicy: false, // Deshabilita si tienes CSP propio o scripts inline
crossOriginEmbedderPolicy: false,
}));
Content Security Policy (CSP): el más poderoso y complejo
CSP es el header más efectivo contra XSS (Cross-Site Scripting), pero también el más difícil de configurar porque bloquea scripts, estilos e imágenes que no estén en la lista blanca.
Un CSP básico para una app con scripts de tu propio dominio:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;
Empieza en modo reporte (Content-Security-Policy-Report-Only) para detectar qué romperías antes de activarlo en producción:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Verifica tu configuración
Una vez desplegado, usa estas herramientas para verificar:
- securityheaders.com — analiza los headers de cualquier URL
- SSL Labs — califica tu configuración TLS
- Mozilla Observatory — análisis completo de seguridad
Un sitio bien configurado debería obtener al menos B+ en Security Headers y A en SSL Labs. No es perfección, es el mínimo razonable para cualquier aplicación en producción.