Seguridad web: buenas prácticas capa por capa (guía 2026)

Advertisement
Qué significan de verdad las buenas prácticas de seguridad web
Las buenas prácticas de seguridad web son el conjunto de controles que impiden que un sitio o una aplicación web acabe en manos de un atacante, sea desfigurado, se use para atacar a sus propios visitantes o pierda datos sin que nadie lo note. No son un producto ni un ajuste concreto. Son una pila de decisiones que empieza en el registrador del dominio y sigue por el DNS, el TLS, las cabeceras HTTP que envía tu servidor, el código que gestiona los inicios de sesión y las entradas, los paquetes de terceros sobre los que construyes, el propio servidor y, por último, los registros que te avisan de que algo ha ido mal.
La razón para pensar en capas es que los atacantes lo hacen. El Data Breach Investigations Report 2026 de Verizon concluyó que el 31 % de las brechas empieza ahora con la explotación de una vulnerabilidad de software, que por primera vez supera a las credenciales robadas como vía de entrada más habitual, y que el 48 % de todas las brechas implica ransomware. Basta un solo parche sin aplicar, una clave de API filtrada o una página de administración sin rate limiting. Ninguno de los controles de esta guía es exótico: los sitios que sufren brechas suelen ser los que se saltaron lo básico en una capa mientras pulían otra.
Esta guía va de fuera hacia dentro. Cada práctica explica por qué importa, qué configurar exactamente y cómo verificarlo con un comando o una herramienta gratuita, porque el fallo más común que vemos en las comprobaciones de cabeceras HTTP y las comprobaciones SSL no es una mala decisión, sino un ajuste que alguien creía activado y nunca confirmó.
La pila de seguridad web: seis capas que proteger
Todo ataque a un sitio web recae en una de seis capas. La tabla siguiente es el mapa del resto de la guía: qué vive en cada capa, cómo suele atacarse y qué comprobación gratuita confirma que tu defensa está realmente en su sitio.
| Capa | Qué hacen aquí los atacantes | Prácticas clave | Verificar con |
|---|---|---|---|
| 1. Dominio y DNS | Secuestran el dominio, cambian los servidores de nombres, toman subdominios huérfanos, obtienen certificados fraudulentos | Registrar lock, MFA, DNSSEC, CAA, auditoría de registros obsoletos | Consulta WHOIS, Consulta DNS, Buscador de Subdominios |
| 2. Transporte (TLS) | Degradan la conexión a HTTP, eliminan el cifrado, explotan cifrados antiguos, aprovechan certificados caducados | TLS 1.2+, HSTS con preload, renovación automatizada | Verificador de Certificado SSL |
| 3. Cabeceras HTTP | Inyectan scripts (XSS), enmarcan el sitio para hacer clickjacking, provocan MIME sniffing, filtran el referrer | CSP, frame-ancestors, nosniff, Referrer-Policy, Permissions-Policy | Verificador de Cabeceras HTTP |
| 4. Aplicación | Credential stuffing, inyección, control de acceso roto, CSRF, subidas de archivos inseguras | Argon2id + MFA + límites de tasa, consultas parametrizadas, autorización en el servidor, cookies SameSite | Revisión de código, OWASP ZAP, Prueba de Fuerza de Contraseña |
| 5. Dependencias | Paquetes envenenados, CVE conocidos en bibliotecas, pipelines de CI comprometidos | Lockfiles, auditorías, versiones fijadas, SBOM, tokens de mínimo privilegio | npm audit, Dependabot, Trivy |
| 6. Servidor y operaciones | Puertos abiertos, credenciales por defecto, parches pendientes, sin copias de seguridad, sin registros | Firewall, SSH solo con claves, parches, copias 3-2-1, registro centralizado | Verificador de Puertos, Verificador de Lista Negra IP, Verificador de Salud del Dominio |
Advertisement
Capa 1: protege el dominio y el DNS
Todo lo demás en esta guía da por hecho que sigues controlando tu dominio. Si un atacante puede entrar en tu cuenta del registrador o cambiar tus servidores de nombres, puede dirigir tu tráfico a cualquier parte, obtener un certificado válido para tu nombre y leer todo el correo que te envíen, sin que tu código de aplicación llegue a ejecutarse nunca. Por eso la seguridad del dominio y del DNS es la primera buena práctica de seguridad web, y es casi por completo una cuestión de configuración.
Empieza con una consulta WHOIS de tu propio dominio y confirma tres cosas: que los códigos de estado incluyen clientTransferProhibited, que la fecha de caducidad está a más de un año vista y que el registrador es uno en el que realmente tienes cuenta. Es sorprendentemente habitual que el dominio de una empresa siga en la cuenta de registrador de un antiguo proveedor externo. Nuestra guía de consulta WHOIS explica cada código de estado que verás.
Registrar lock, MFA y alertas de caducidad
Activa el registrar lock (también llamado bloqueo de transferencia) para que el dominio no pueda trasladarse a otro registrador sin que lo desbloquees explícitamente, y activa la autenticación multifactor (MFA) en la propia cuenta del registrador. Las cuentas de registrador son un objetivo favorito del phishing precisamente porque un solo inicio de sesión controla todo lo que depende de ellas. Usa una llave de hardware o una app de autenticación en lugar de SMS siempre que el registrador lo permita.
Configura la renovación automática del dominio con un método de pago que no vaya a caducar y, aun así, añade una alerta en el calendario 60 días antes de la fecha de caducidad. Los drop-catchers se hacen con los dominios caducados en cuestión de horas, y el comprador hereda tu correo, tu tráfico y cualquier inicio de sesión OAuth que confíe en tu dominio.
El registry lock (un bloqueo manual, fuera de banda, a nivel del registro) compensa la tarifa extra en un dominio del que depende un negocio: impide que incluso una cuenta de registrador comprometida cambie los servidores de nombres.
Separa los roles. La persona que paga el dominio, la cuenta que lo posee y el proveedor de DNS deben estar documentados y no depender del buzón de un solo empleado.
Activa la privacidad WHOIS para que tus datos de contacto no sirvan de mapa para el phishing, y mantén en la cuenta una dirección de rol supervisada, como
domains@.
Firma la zona con DNSSEC
DNSSEC firma tu zona DNS para que un resolver pueda detectar respuestas falsificadas. Sin él, un atacante capaz de envenenar la caché de un resolver puede enviar a tus visitantes a un servidor falso mientras su navegador sigue mostrando tu nombre de dominio. La mayoría de los proveedores de DNS gestionado (Cloudflare, Route 53, Google Cloud DNS y muchos registradores) lo activan con un solo interruptor; el único paso manual es publicar el registro DS en tu registrador. Verifícalo con dig o con la comprobación DNSSEC del Verificador de Salud del Dominio:
dig +dnssec +short yourdomain.com A
# A signed zone returns the record AND an RRSIG line:
# 203.0.113.10
# A 13 2 3600 20261015000000 20260924000000 34505 yourdomain.com. Kx3f...==
dig +short yourdomain.com DS
# A DS record at the parent zone proves the chain of trust is complete
# 34505 13 2 6A1B...C9Restringe la emisión de certificados con CAA
Un registro CAA (RFC 8659) indica a las autoridades de certificación cuáles de ellas pueden emitir certificados para tu dominio. Toda CA pública está obligada a consultarlo antes de emitir, así que un registro CAA le cierra la puerta a un atacante que haya engañado a la validación de dominio de otra CA. Tres registros cubren los casos habituales: quién puede emitir, quién puede emitir certificados wildcard y dónde notificar una solicitud rechazada:
yourdomain.com. CAA 0 issue "letsencrypt.org"
yourdomain.com. CAA 0 issuewild ";"
yourdomain.com. CAA 0 iodef "mailto:security@yourdomain.com"Audita los registros obsoletos: subdomain takeover
Un subdomain takeover (toma de control de un subdominio) ocurre cuando un registro DNS sigue apuntando a un servicio que ya no usas: un CNAME a un sitio de GitHub Pages eliminado, una app de Azure, un bucket de S3 o el hostname de un proveedor SaaS. Cualquiera puede registrar el destino abandonado y servir contenido en old-app.yourdomain.com en tu nombre, con certificado válido incluido. Es uno de los hallazgos más comunes en los programas de bug bounty, porque nadie recuerda que el registro existe.
Pasa tu dominio por el Buscador de Subdominios, que lee los registros de Certificate Transparency, para ver todos los hostnames para los que se ha emitido alguna vez un certificado. Después resuelve cada uno con una consulta DNS y elimina cualquier registro cuyo destino devuelva NXDOMAIN o la página "no such app" de un proveedor. Repítelo cada trimestre y haz que eliminar el registro DNS forme parte de la retirada de cualquier servicio.
Capa 2: HTTPS y TLS bien configurados
HTTPS ya no es una buena práctica: es el mínimo, y los navegadores marcan las páginas HTTP sin cifrar como "No es seguro". Lo que todavía separa un sitio seguro de uno que simplemente está cifrado son las versiones de TLS que aceptas, si queda alguna forma de llegar a ti por HTTP y si la renovación está lo bastante automatizada como para sobrevivir a los recortes de vida útil de los certificados que empezaron en marzo de 2026.
Advertisement
TLS 1.2 como mínimo, TLS 1.3 preferido
TLS 1.0 y 1.1 quedaron formalmente obsoletos con el RFC 8996 en 2021, y ningún navegador actual los negocia. Desactívalos en el servidor, da preferencia a TLS 1.3 (que elimina todos los cifrados débiles conocidos y completa el handshake en un solo viaje de ida y vuelta) y mantén TLS 1.2 solo con suites de cifrado AEAD como ECDHE-ECDSA-AES128-GCM-SHA256 o ECDHE-RSA-CHACHA20-POLY1305.
Cuando pruebas desde fuera importan dos líneas: el protocolo negociado y Verify return code: 0, que significa que la cadena de certificados completa se ha validado. La falta de un certificado intermedio es la causa más habitual de los casos de "funciona en Chrome, falla en curl y en Android"; nuestra guía sobre la cadena de certificados SSL explica cómo arreglarlo, y el Verificador de Certificado SSL señala directamente una cadena incompleta. Esta es la comprobación ejecutada contra dnsrobot.net:
echo | openssl s_client -connect dnsrobot.net:443 -servername dnsrobot.net 2>/dev/null \
| grep -E 'Protocol|Cipher|Verify return'
# Protocol : TLSv1.3
# Cipher : TLS_AES_256_GCM_SHA384
# Verify return code: 0 (ok)
# Confirm the old versions are refused:
openssl s_client -connect dnsrobot.net:443 -tls1_1 2>&1 | grep -iE 'alert|no protocols'HSTS: que el navegador no vuelva a usar HTTP
Redirigir HTTP a HTTPS no basta por sí solo. La primera petición que hace un visitante a http://yourdomain.com sigue viajando en texto plano, y un atacante en la misma red puede responderla antes que tu redirección. HTTP Strict Transport Security (RFC 6797) lo soluciona: en cuanto un navegador ha visto la cabecera, reescribe cualquier URL http:// futura de tu dominio a https:// antes de enviar nada.
Usa un max-age de al menos un año (31536000 segundos), añade includeSubDomains cuando todos los subdominios sirvan HTTPS y, después, envía el dominio a hstspreload.org para que venga integrado de fábrica en Chrome, Firefox, Safari y Edge. El preload cierra por completo el hueco de la primera visita. Verifica la cabecera con el Verificador de Cabeceras HTTP, que evalúa HSTS como parte de su nota de seguridad de la A a la F.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadCertificados de 47 días: automatiza ya la renovación
La votación SC-081 del CA/Browser Forum está reduciendo por fases la vida útil máxima de todos los certificados TLS públicos. El primer recorte entró en vigor el 15 de marzo de 2026, así que cualquier certificado que compres o renueves hoy ya está limitado a 200 días, y en 2029 habrá que sustituir un certificado aproximadamente cada seis semanas:
| Fecha de entrada en vigor | Validez máxima del certificado | Reutilización de la validación de dominio |
|---|---|---|
| Antes del 15 de marzo de 2026 | 398 días | 398 días |
| 15 de marzo de 2026 | 200 días | 200 días |
| 15 de marzo de 2027 | 100 días | 100 días |
| 15 de marzo de 2029 | 47 días | 10 días |
La renovación manual no sobrevive a este calendario. La buena práctica es la emisión totalmente automatizada mediante ACME: Let's Encrypt o Google Trust Services con certbot, acme.sh o Caddy, o los certificados automáticos integrados en Cloudflare, Vercel, Netlify y la mayoría de los paneles de hosting. Prueba la renovación antes de necesitarla (certbot renew --dry-run en el caso de certbot); el fallo que hay que buscar es un registro CAA o una regla de firewall añadidos desde la última renovación que ahora bloquean la validación. Uses lo que uses, vigila también la fecha de caducidad desde fuera: el Verificador de Certificado SSL muestra los días restantes, y un certificado caducado convierte cada visita en un aviso del navegador a pantalla completa.
Capa 3: cabeceras de seguridad HTTP
Las cabeceras de seguridad son instrucciones que tu servidor da al navegador sobre lo que puede y no puede hacer con tus páginas: qué scripts pueden ejecutarse, si la página puede cargarse dentro de un marco, si el navegador puede adivinar tipos de contenido y qué debe contar a otros sitios sobre la procedencia de un visitante. No cuestan nada, se aplican a todas las páginas a la vez y bloquean clases enteras de ataques. También son la capa que la mayoría de los sitios configura solo a medias, y por eso el Verificador de Cabeceras HTTP las califica. Esto es lo que envía dnsrobot.net, recortado a las cabeceras de seguridad:
curl -sI https://dnsrobot.net/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self' ... https://*.googletagmanager.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(self), microphone=(), geolocation=(), payment=(), usb=()Las siete cabeceras que todo sitio debería enviar
Estos son los valores a los que aspirar en un sitio típico. Las cinco primeras son las que mueven tu nota; las dos últimas son añadidos baratos una vez que las demás están en su sitio.
| Cabecera | Valor recomendado | Qué evita |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | Degradación de protocolo, robo de cookies a través de HTTP |
Content-Security-Policy | script-src basado en nonces con 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none' | Cross-site scripting (XSS), inyección de datos, clickjacking |
X-Content-Type-Options | nosniff | MIME sniffing que convierte un archivo subido en un script ejecutable |
Referrer-Policy | strict-origin-when-cross-origin | Filtración de URL completas (tokens, términos de búsqueda) a terceros |
Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=() | Scripts de terceros que usan sin avisar APIs potentes del navegador |
X-Frame-Options | DENY (respaldo heredado para frame-ancestors) | Clickjacking en navegadores sin CSP Level 2 |
Cross-Origin-Opener-Policy | same-origin | Ataques entre ventanas a través de window.opener |
Content Security Policy sin romper el sitio
Una Content Security Policy es la defensa más eficaz contra XSS, porque impide que los scripts inyectados se ejecuten incluso cuando existe un fallo de inyección. El problema es que una política estricta rompe cualquier script inline o etiqueta de terceros que se te haya olvidado. El enfoque moderno, recomendado por Google y documentado en MDN, es una política basada en nonces: el servidor genera un nonce aleatorio por respuesta, lo pone en cada etiqueta script que renderiza de forma intencionada y el navegador rechaza todo lo demás.
'strict-dynamic' es lo que hace práctica la política: un script con nonce puede cargar más scripts (analítica, gestores de etiquetas, widgets) sin que haya que añadir cada uno a una lista de permitidos, mientras que los navegadores modernos ignoran los tokens https: y 'unsafe-inline', que solo sirven de respaldo para los antiguos. Despliégala primero con Content-Security-Policy-Report-Only, revisa los informes de infracciones durante una semana y luego pasa al modo de aplicación. Frameworks como Next.js, Rails y Django tienen soporte nativo para nonces; los sitios estáticos pueden usar en su lugar un script-src basado en hashes.
Content-Security-Policy:
script-src 'nonce-r4nd0m1z3dV4lu3' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpoint
<!-- Only scripts carrying the nonce run; strict-dynamic lets them load their own dependencies -->
<script nonce="r4nd0m1z3dV4lu3" src="/app.js"></script>Clickjacking: frame-ancestors y X-Frame-Options
El clickjacking carga tu sitio de forma invisible dentro de la página de un atacante y engaña al visitante para que haga clic en un botón que no ve. frame-ancestors 'none' en la CSP (o 'self' si incrustas tus propias páginas) lo impide en todos los navegadores actuales, y X-Frame-Options: DENY cubre los más antiguos. Si ofreces a propósito un widget incrustable, exime solo esa ruta y solo para los orígenes que lo necesiten; nuestra guía de X-Frame-Options repasa las reglas de servidor exactas y los errores "refused to connect" que te encontrarás al hacer pruebas.
nosniff, Referrer-Policy, Permissions-Policy y COOP
X-Content-Type-Options: nosniff impide que el navegador cuestione un Content-Type, que es lo que convierte una "imagen" subida por un usuario que contiene HTML en una página ejecutable. Referrer-Policy: strict-origin-when-cross-origin (el valor por defecto en los navegadores actuales, pero defínelo explícitamente) envía a otros sitios solo tu origen, de modo que los tokens de restablecimiento de contraseña y las búsquedas incluidas en las URL se mantienen privados. Permissions-Policy desactiva las funciones del navegador que no usas, para que un script de terceros comprometido no pueda abrir la cámara ni leer la ubicación. Cross-Origin-Opener-Policy: same-origin corta el vínculo window.opener con las páginas que abres, lo que bloquea toda una clase de ataques entre ventanas.
Las cabeceras que hay que eliminar también importan: Server y X-Powered-By anuncian versiones exactas de software a los escáneres de vulnerabilidades, y X-XSS-Protection está obsoleta y puede introducir fallos en navegadores antiguos, así que ponla a 0 o elimínala. El Detector de CMS muestra lo que un escáner averigua sobre tu stack solo a partir de las cabeceras.
Capa 4: una autenticación que resista el credential stuffing
Los atacantes rara vez adivinan contraseñas de una en una. Prueban miles de millones de pares de correo y contraseña filtrados de otros sitios (credential stuffing) y consiguen el resto con phishing. Por eso las buenas prácticas de autenticación tienen tres partes: almacenar las contraseñas de modo que una fuga de la base de datos no sea una fuga de contraseñas, hacer que una contraseña robada no sirva por sí sola y hacer que la fuerza bruta sea demasiado lenta para importar. OWASP sitúa los Authentication Failures (fallos de autenticación) en el puesto A07 del OWASP Top 10:2025.
Advertisement
Hash de contraseñas con Argon2id o bcrypt
Nunca almacenes una contraseña, y nunca almacenes un hash rápido de ella. MD5, SHA-1 e incluso SHA-256 están diseñados para ser rápidos, así que una tabla filtrada de estos hashes puede probarse a miles de millones de intentos por segundo con una sola GPU. Usa una función de hash de contraseñas lenta y con uso intensivo de memoria, con una sal única por contraseña. La OWASP Password Storage Cheat Sheet recomienda, por este orden:
Argon2id con al menos 19 MiB de memoria, 2 iteraciones y 1 grado de paralelismo (o 46 MiB con 1 iteración).
scrypt con N = 2^17, r = 8, p = 1 cuando Argon2 no esté disponible.
bcrypt con un factor de trabajo de 10 o más (ten en cuenta su límite de entrada de 72 bytes; aplicar un hash previo a las contraseñas más largas requiere cuidado).
PBKDF2-HMAC-SHA256 con 600.000 iteraciones, solo cuando el cumplimiento de FIPS lo exija.
// Node.js with the argon2 package
import argon2 from "argon2";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // KiB = 19 MiB
timeCost: 2,
parallelism: 1,
});
// Later, on login:
const ok = await argon2.verify(hash, submittedPassword);Reglas de contraseñas que sí ayudan (NIST SP 800-63B)
Las reglas de composición que la mayoría de los sitios siguen imponiendo (una mayúscula, un símbolo, cambio cada 90 días) generan contraseñas predecibles como Summer2026! y empujan a la gente a reutilizarlas. La guía actual del NIST SP 800-63B las sustituye por reglas que miden lo que importa:
Mínimo de 8 caracteres cuando también se exige MFA, y al menos 15 caracteres para una contraseña que se usa sola; admite como mínimo 64.
Sin reglas de composición y sin rotación periódica forzada; exige un cambio solo cuando haya indicios de que la cuenta está comprometida.
Comprueba cada contraseña nueva contra una lista de filtraciones (la API de rangos de Have I Been Pwned permite hacerlo sin enviar la contraseña) y rechaza las que ya se hayan filtrado.
Permite pegar y usar gestores de contraseñas, acepta espacios y Unicode, y muestra un medidor de fortaleza basado en la entropía y no en reglas.
Nuestra Prueba de Fuerza de Contraseña muestra en qué se diferencian estas medidas de las reglas antiguas: estima el tiempo que se tardaría en descifrarla a partir de la entropía y la detección de patrones, y comprueba la contraseña contra datos de filtraciones, lo que resulta mucho más útil que un mensaje en rojo de "falta un símbolo".
MFA y passkeys
La autenticación multifactor convierte una contraseña robada en un callejón sin salida. Ofrécela a todo el mundo y exígela para los roles de administración, finanzas y soporte. Ordena las opciones por su resistencia al phishing: las passkeys y las llaves de seguridad FIDO2 no pueden robarse con phishing porque la credencial está vinculada a tu origen real; las apps de autenticación (TOTP) son buenas; los códigos por SMS son mejor que nada, pero pueden interceptarse mediante SIM swapping. Las passkeys funcionan en todos los navegadores y sistemas operativos actuales y eliminan por completo la contraseña, lo que también elimina el credential stuffing como categoría.
Protege la vía de recuperación con el mismo cuidado que el inicio de sesión: códigos de recuperación almacenados con hash, restablecimientos por correo con tokens de un solo uso que caduquen en menos de 15 minutos y nada de preguntas de seguridad.
Rate limiting, bloqueo de cuentas y defensa contra bots
Aplica rate limiting (limitación de tasa) a los endpoints de inicio de sesión, registro, restablecimiento de contraseña y MFA por IP y por cuenta, y devuelve 429 Too Many Requests con una cabecera Retry-After cuando se alcance el límite (nuestra guía sobre el error HTTP 429 explica cómo deben reaccionar los clientes). Combínalo con un retardo progresivo o un bloqueo temporal tras fallos repetidos en una misma cuenta, y con un CAPTCHA o un reto de prueba de trabajo en los endpoints que reciben ataques distribuidos. Registra cada inicio de sesión fallido con la IP de origen para que el patrón de una campaña de credential stuffing sea visible en minutos y no en meses.
# nginx: 5 login attempts per minute per client IP
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /api/login {
limit_req zone=login burst=4 nodelay;
limit_req_status 429;
proxy_pass http://app;
}Sesiones, cookies y CSRF
Tras el inicio de sesión, la cookie de sesión es el usuario, así que merece la misma protección que la contraseña. Tres atributos de cookie y un prefijo de nombre hacen la mayor parte del trabajo:
Securesignifica que la cookie nunca se envía por HTTP sin cifrar, así que no puede interceptarse en una red Wi-Fi pública.HttpOnlyla hace invisible para JavaScript, así que un fallo XSS no puede leerla.SameSite=Lax(oStrictpara los paneles de administración) evita que viaje en peticiones POST entre sitios, lo que neutraliza la mayoría de los ataques CSRF.El prefijo
__Host-hace que el navegador rechace la cookie salvo que seaSecure, tengaPath=/y no lleve atributoDomain, de modo que un subdominio no puede sobrescribirla.
Set-Cookie: __Host-session=9f1c7e2a...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800CSRF: una segunda línea de defensa detrás de SameSite
El cross-site request forgery (CSRF) engaña a un navegador con sesión iniciada para que envíe a tu sitio, desde otra página, una petición que cambia el estado. Las cookies SameSite detienen el caso común, pero mantén una segunda defensa en cada petición que no sea GET: un token anti-CSRF por sesión en un campo oculto o en una cabecera personalizada, o una comprobación de que la cabecera Origin (o Sec-Fetch-Site) coincide con tu propio origen. La mayoría de los frameworks (Django, Rails, Laravel, las server actions de Next.js) lo hacen por defecto; el error es desactivarlo para un endpoint de API y luego llamar a ese endpoint desde un navegador.
IDs de sesión, rotación y JWT
Genera los IDs de sesión con al menos 128 bits de aleatoriedad, rota el ID al iniciar sesión y ante cualquier cambio de privilegios para frustrar la fijación de sesión, haz caducar las sesiones tras un periodo de inactividad y ofrece a los usuarios un botón de "cerrar sesión en todos los dispositivos" que las invalide en el servidor. En el caso de los JWT, dales una vida corta (minutos, con un refresh token revocable), no los guardes nunca en localStorage, donde cualquier script puede leerlos, y trata una clave de firma filtrada como una brecha completa, porque cualquier token que haya firmado pasa a poder falsificarse.
Inyección y XSS: valida la entrada, codifica la salida
La inyección (A05:2025) y el cross-site scripting son el mismo error en lugares distintos: los datos de un usuario se entregan a un intérprete (SQL, la shell, una consulta LDAP, una página HTML) como si fueran código. La solución también es la misma en todas partes: mantener separados los datos y el código, y no construir nunca un comando concatenando cadenas.
Advertisement
Consultas parametrizadas, no cadenas concatenadas
En SQL, usa sentencias preparadas o un ORM que las genere; así la base de datos trata la entrada como un valor que nunca puede cambiar la estructura de la consulta. La misma regla se aplica a los comandos de shell (pasa un array de argumentos, nunca una cadena), a los filtros NoSQL (rechaza objetos de operador como {"$gt": ""} en la entrada del usuario) y a las rutas de archivo (resuelve la ruta y confirma que se queda dentro del directorio previsto).
// Vulnerable: the input becomes part of the query text
const rows = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Safe: the driver sends the value separately from the query
const rows = await db.query("SELECT * FROM users WHERE email = $1", [email]);
// Python / psycopg: same idea
cur.execute("SELECT * FROM users WHERE email = %s", (email,))La codificación de salida detiene el XSS
El cross-site scripting se produce cuando un texto controlado por el usuario se escribe en una página sin codificarlo para el contexto en el que acaba. Los motores de plantillas modernos (React y JSX, Vue, Jinja2, Blade, ERB) codifican por defecto; hoy el XSS suele venir de las vías de escape: dangerouslySetInnerHTML, v-html, |safe, innerHTML y la construcción de URL o manejadores de eventos inline a partir de datos del usuario. Codifica para el contexto exacto y sanea el HTML enriquecido con una biblioteca creada para ello, como DOMPurify, en lugar de con una expresión regular.
| Dónde acaban los datos | Codificar como | Ejemplo |
|---|---|---|
| Cuerpo HTML | Entidades HTML | < pasa a ser <, " pasa a ser " |
| Atributo HTML | Entidades con el atributo entre comillas | Pon siempre el atributo entre comillas; codifica " y ' |
| JavaScript | No inyectes en el texto del script; pasa los datos mediante atributos data- o un bloque JSON <script type="application/json"> | </script> dentro de una cadena pasa a ser \u003c/script\u003e |
| Parámetro de URL | Codificación porcentual | encodeURIComponent(value) |
| Valor CSS | Evítalo por completo; elige nombres de clase de una lista de permitidos | Nunca interpoles entrada del usuario en style |
SSRF, subida de archivos y deserialización
Server-side request forgery (SSRF): si tu servidor descarga una URL proporcionada por un usuario (webhooks, proxies de imágenes, vistas previas de enlaces), un atacante puede apuntarla a servicios internos o al endpoint de metadatos de la nube 169.254.169.254 y leer credenciales. Resuelve primero el hostname, rechaza los rangos privados y link-local, desactiva las redirecciones y coloca los procesos que hacen estas peticiones en un segmento de red que no pueda llegar a nada interno.
Subida de archivos: valida el tipo inspeccionando el contenido, no la extensión ni el Content-Type que envía el cliente; impón un límite de tamaño; guarda los archivos fuera de la raíz web o en almacenamiento de objetos con un nombre aleatorio generado por ti; y sírvelos desde un origen separado con nosniff y Content-Disposition: attachment siempre que sea posible. No dejes nunca que un archivo subido acabe en un lugar donde el servidor web vaya a ejecutarlo.
Deserialización y asignación masiva: no deserialices nunca datos no fiables con un formato que pueda instanciar clases arbitrarias (la serialización nativa de Java, pickle de Python, unserialize de PHP, YAML con etiquetas personalizadas); usa JSON con un esquema. Vincula los cuerpos de las peticiones a una lista explícita de campos permitidos para que un usuario no pueda enviar por POST "role": "admin" a un modelo que casualmente tenga esa columna.
Broken Access Control: el riesgo número uno
Broken Access Control (control de acceso roto) ocupa el primer puesto del OWASP Top 10 desde 2021 y lo mantiene como A01:2025. No es un fallo concreto, sino un hábito: comprobar quién es alguien (autenticación) y olvidarse de comprobar qué puede tocar (autorización) en cada petición. La forma clásica es la referencia directa insegura a objetos (IDOR): GET /api/invoices/1042 funciona para el propietario de la factura, y también para cualquiera que cambie el número.
Deniega por defecto. Cada ruta requiere una regla explícita que conceda acceso; si falta la regla, la respuesta es 403, no 200.
Aplícalo en el servidor, objeto por objeto. Ocultar un botón en la interfaz no es control de acceso. Cada lectura y escritura debe confirmar que el usuario actual es el propietario del registro concreto o tiene permiso sobre él, también en los endpoints masivos, las exportaciones y las tareas en segundo plano.
Usa IDs imposibles de adivinar cuando ayude (UUID), pero no confíes nunca en ellos: la ocultación no es autorización.
Restringe CORS.
Access-Control-Allow-Origin: *con credenciales, o reflejar cualquierOriginque envíe la petición, pone tu API en manos de cualquier sitio web que visite el usuario.Desactiva el listado de directorios, bloquea
.git,.env, las copias de seguridad y los archivos de configuración a nivel del servidor web, y mantén los paneles de administración fuera de internet o detrás de MFA y listas de IP permitidas.Aplica rate limiting y registra los fallos de autorización. Una ráfaga de 403 desde una misma sesión es un ataque de enumeración en curso.
// Express: ownership check on every object access
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
if (!invoice || invoice.ownerId !== req.user.id) {
return res.status(404).end(); // 404, not 403: don't confirm the record exists
}
res.json(invoice);
});Capa 5: dependencias y cadena de suministro de software
Una aplicación web típica incluye quizá un 5 % de código escrito por ti y un 95 % de código descargado. Por eso Software Supply Chain Failures (fallos en la cadena de suministro de software) debuta en el puesto A03 del OWASP Top 10 de 2025, y por eso la explotación de vulnerabilidades superó a las credenciales robadas en el DBIR de 2026. En septiembre de 2025, un solo mantenedor víctima de phishing introdujo malware para robar criptomonedas en chalk, debug y otros 16 paquetes npm que juntos suman más de dos mil millones de descargas a la semana; todos los proyectos que instalaron una versión sin fijar durante ese periodo lo incorporaron automáticamente.
Haz commit de un lockfile (
package-lock.json,yarn.lock,pnpm-lock.yaml,poetry.lock,go.sum) e instala connpm cien CI para que las compilaciones sean reproducibles y una nueva versión upstream no pueda colarse sin revisión.Analiza de forma continua:
npm audit,pip-audit,bundler-audit, GitHub Dependabot o Renovate para las actualizaciones, y un escáner de contenedores como Trivy o Grype para la capa del sistema operativo.Retrasa unos días las actualizaciones que no sean de seguridad (
minimumReleaseAgede Renovate) para que una versión envenenada suela retirarse antes de que la adoptes, pero aplica los parches de seguridad de inmediato.Fija las actions y los scripts de terceros a un SHA de commit en CI, y usa Subresource Integrity (
integrity="sha384-...") en cualquier script que cargues desde una CDN pública.Da a CI el mínimo privilegio: tokens de solo lectura cuando sea posible, credenciales OIDC de corta duración en lugar de secretos de larga duración y permisos de publicación limitados a un job de release.
Genera un SBOM (CycloneDX o SPDX) en tiempo de compilación para poder responder "¿nos afecta?" en minutos cuando llegue el próximo CVE del calibre de Log4Shell.
# Reproducible install + audit in CI
npm ci --ignore-scripts
npm audit --audit-level=high
# Pin a GitHub Action to a commit, not a floating tag
# uses: actions/checkout@v4 <- can change underneath you
# uses: actions/checkout@<full-commit-sha> # v4.2.2
# Subresource Integrity for a CDN script (hash from: openssl dgst -sha384 -binary lib.min.js | openssl base64 -A)
<script src="https://cdn.example.com/lib.min.js"
integrity="sha384-<base64-hash>"
crossorigin="anonymous"></script>Si usas WordPress u otro CMS, las mismas reglas se aplican a los plugins y temas, que es donde empiezan la mayoría de los compromisos de un CMS. Mantén actualizados el núcleo y todos los plugins, elimina los que no uses y comprueba lo que un escáner puede averiguar sobre tus versiones con el Detector de CMS.
Capa 6: hardening del servidor (puertos, parches, secretos y copias de seguridad)
La capa de aplicación da igual si el servidor que hay debajo acepta inicios de sesión SSH con contraseña, ejecuta una base de datos en un puerto público o no se ha parcheado desde que se instaló. Las buenas prácticas de servidor son aburridas, y es justo por ahí por donde entra el ransomware.
Cierra todos los puertos que no uses
Un servidor web público necesita abiertos los puertos 80 y 443, y SSH desde tu propio rango de IP. Las bases de datos (3306, 5432, 27017), Redis (6379), Elasticsearch (9200), los paneles de administración y los endpoints de métricas deben escuchar solo en localhost o en una red privada. Compruébalo desde fuera con el Verificador de Puertos después de cada cambio, porque esa es la vista que tiene un atacante, y los grupos de seguridad en la nube son fáciles de malinterpretar.
# Ubuntu: default-deny firewall, allow only web + SSH from your office range
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp
sudo ufw enable
# SSH: keys only, no root password login
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
sudo systemctl reload sshParchea con calendario y saca los secretos del código
Activa las actualizaciones de seguridad desatendidas del sistema operativo, suscríbete a las listas de seguridad de tu runtime y de tu framework, y trata un CVE crítico en cualquier cosa expuesta a internet como una tarea para el mismo día. Ejecuta los servicios con usuarios sin privilegios, en contenedores o con el sandboxing de systemd, para que un proceso comprometido no pueda leer toda la máquina.
Los secretos (contraseñas de bases de datos, claves de API, claves de firma) nunca deben estar en el repositorio, en una capa de una imagen de Docker ni en JavaScript del lado del cliente. Cárgalos desde variables de entorno inyectadas en el despliegue o desde un gestor de secretos, rótalos cuando se marche alguien con acceso y analiza el historial del repositorio con una herramienta como gitleaks, porque una clave que se ha subido una sola vez queda comprometida para siempre, aunque se elimine el commit.
Copias de seguridad que sobrevivan al ransomware y una CDN delante
Sigue la regla 3-2-1: tres copias, en dos soportes distintos y una fuera de las instalaciones, y haz que al menos una copia sea inmutable o esté desconectada para que un ransomware que llegue al servidor no pueda cifrar también las copias. Cifra las copias de seguridad, incluye la base de datos y el directorio de subidas y, sobre todo, prueba una restauración de forma periódica. Una copia de seguridad que nadie ha restaurado nunca es una esperanza, no un plan. El tiempo de recuperación es lo que decide si un incidente es una simple caída del servicio o algo que acaba con la empresa.
Coloca una CDN o un WAF (Cloudflare, Fastly, AWS CloudFront con WAF o el equivalente de tu proveedor de hosting) delante del origen. Absorbe los DDoS volumétricos, aplica reglas gestionadas contra las cargas de inyección habituales y los bots maliciosos, oculta la IP de tu origen y te da un lugar donde aplicar límites de tasa y reglas geográficas sin tocar la aplicación. Después, restringe el firewall del origen para que solo los rangos de IP de la CDN puedan llegar directamente al puerto 443; de lo contrario, los atacantes simplemente la esquivan. El Comprobador de Hosting muestra si el origen real de un sitio queda expuesto detrás de su CDN.
Autenticación del correo: SPF, DKIM y DMARC
La seguridad web incluye el correo que transporta tus restablecimientos de contraseña, tus facturas y tus respuestas de soporte. Sin SPF, DKIM y DMARC, cualquiera puede enviar correo como billing@yourdomain.com, y desde febrero de 2024 Google y Yahoo exigen los tres a los remitentes masivos. El conjunto completo son tres registros TXT de DNS:
yourdomain.com. TXT "v=spf1 include:_spf.google.com -all"
google._domainkey.yourdomain.com. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
_dmarc.yourdomain.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc@yourdomain.com; adkim=s; aspf=s"Empieza DMARC con p=none para recopilar informes y pasa a p=quarantine y luego a p=reject cuando todos los remitentes legítimos (plataforma de marketing, helpdesk, CRM) superen la alineación. Añade registros MTA-STS y TLS-RPT para forzar la entrega cifrada a tus servidores de correo, y publica un MX nulo (MX 0 .) en los dominios que nunca envían correo para que no puedan suplantarse en absoluto. Verifica cada registro con el Verificador de Registro SPF, el Verificador DKIM y el Verificador DMARC, y si baja la entregabilidad, comprueba tus IP de envío en las listas de bloqueo con el Verificador de Lista Negra IP.
Registra, monitoriza y prepárate para la brecha
Dos de las diez categorías de OWASP 2025 tratan de lo que ocurre después de un error: Security Logging and Alerting Failures (fallos de registro y alertas de seguridad, A09) y la nueva Mishandling of Exceptional Conditions (gestión incorrecta de condiciones excepcionales, A10). La brecha típica sigue pasando desapercibida durante meses porque las pruebas nunca se registraron, o se registraron y nadie las miró.
Registra los eventos de seguridad con contexto: cada inicio de sesión correcto y fallido, cambios de MFA, restablecimientos de contraseña, cambios de permisos, denegaciones de control de acceso, fallos de validación de entradas y acciones de administración, cada uno con marca de tiempo, ID de usuario, IP de origen y user agent.
No registres nunca secretos: contraseñas, tokens de sesión, números de tarjeta completos, claves de API. Enmascáralos en la capa de registro, no en cada punto de llamada.
Saca los registros del servidor a un almacén central (CloudWatch, Loki, Elastic, un SIEM) con una retención de al menos 90 días, para que un atacante con acceso root no pueda borrar su rastro.
Crea alertas sobre patrones, no sobre eventos aislados: 50 inicios de sesión fallidos en un minuto, un pico de 403 desde una sesión, un nuevo administrador creado fuera del horario laboral, un certificado emitido para tu dominio que no has solicitado.
Falla en cerrado: cuando una comprobación intermedia (servicio de autenticación, limitador de tasa, WAF) devuelve un error, deniega la petición. A10 existe porque muchísimos sistemas conceden el acceso cuando la comprobación que debía bloquearlo lanza una excepción.
Monitoriza desde fuera: disponibilidad, caducidad de certificados, cambios de DNS, inclusiones en listas de bloqueo y regresiones en las cabeceras. El Verificador de Salud del Dominio agrupa las comprobaciones de DNS, SSL, autenticación del correo y cabeceras en una sola puntuación que puedes volver a calcular tras cada despliegue.
Escribe el plan de respuesta a incidentes antes de necesitarlo
Decide de antemano quién está de guardia, cómo rotar cada credencial, cómo poner el sitio en modo de solo lectura, dónde están las copias de seguridad limpias y a qué autoridad o clientes hay que notificar y en cuántas horas (72 según el RGPD). Haz un ejercicio de simulación (tabletop) una vez al año. Los equipos con un plan ensayado se recuperan en días; los que no lo tienen improvisan durante semanas.
Ponlo a prueba: escáneres, pentests y comprobaciones continuas
Todos los controles anteriores pueden verificarse, y la verificación debería automatizarse siempre que sea posible para que no se deteriore con el tiempo. Una escalera razonable, desde lo gratuito e instantáneo hasta lo periódico y de pago:
| Comprobación | Herramienta | Frecuencia |
|---|---|---|
| Configuración TLS, cadena, caducidad | Verificador de Certificado SSL, SSL Labs, testssl.sh | Tras cada cambio de certificado o de servidor; la caducidad, a diario |
| Cabeceras de seguridad y CSP | Verificador de Cabeceras HTTP, MDN HTTP Observatory | Tras cada despliegue (inclúyelo en CI) |
| Puertos abiertos | Verificador de Puertos, nmap | Tras cada cambio de firewall o de la nube |
| DNS, DNSSEC, autenticación del correo | Verificador de Salud del Dominio, Verificador DMARC | Cada mes y tras cualquier cambio de DNS |
| Subdominios obsoletos | Buscador de Subdominios | Cada trimestre y en cada retirada de un servicio |
| Dependencias con vulnerabilidades conocidas | npm audit, Dependabot, Trivy | En cada compilación |
| Fallos a nivel de código (SAST) | Semgrep, CodeQL | En cada pull request |
| Vulnerabilidades web en tiempo de ejecución (DAST) | OWASP ZAP, Nuclei | Cada semana, contra el entorno de staging |
| Autorización y lógica de negocio | Pentest manual o programa de bug bounty | Una vez al año y tras cada funcionalidad importante |
Los escáneres son buenos detectando inyecciones, cabeceras y componentes desactualizados, y malos con la autorización y la lógica de negocio, así que reserva presupuesto para al menos una prueba humana al año en cualquier cosa que maneje dinero o datos personales. Corrige según la exposición: lo que un usuario no autenticado pueda explotar desde internet va primero.
Checklist de seguridad web
Copia esto en el README de tu proyecto o en tu gestor de tickets. Cada línea es una de las prácticas anteriores, redactada para que pueda marcarse como hecha o pendiente; la última columna es la prueba.
| Capa | Práctica | Hecho cuando |
|---|---|---|
| Dominio | Registrar lock y MFA en la cuenta del registrador; renovación automática activada | WHOIS muestra clientTransferProhibited; caducidad a más de 12 meses vista |
| DNS | Zona firmada con DNSSEC; registro CAA que solo incluye tus CA | dig +dnssec devuelve RRSIG; registro DS presente en la zona padre |
| DNS | Ningún registro CNAME ni A huérfano | Cada hostname del Buscador de Subdominios resuelve a algo que controlas |
| TLS | Solo TLS 1.2+, TLS 1.3 preferido, cadena completa servida | Verificador de Certificado SSL: cadena completa, TLS 1.0/1.1 rechazados |
| TLS | HSTS con max-age de un año, includeSubDomains y preload | Incluido en hstspreload.org |
| TLS | Renovación automatizada mediante ACME; caducidad monitorizada | certbot renew --dry-run se ejecuta sin errores; alerta configurada a 14 días |
| Cabeceras | CSP (basada en nonces), frame-ancestors, nosniff, Referrer-Policy, Permissions-Policy | Nota A en el Verificador de Cabeceras HTTP |
| Autenticación | Hash con Argon2id o bcrypt; reglas de contraseñas del NIST; comprobación contra listas de filtraciones | Un hash tarda de 100 a 250 ms; sin reglas de composición |
| Autenticación | MFA ofrecida a todos y obligatoria para administradores; passkeys admitidas | Imposible iniciar sesión como administrador sin un segundo factor |
| Autenticación | Endpoints de inicio de sesión, restablecimiento y MFA con rate limiting por IP y por cuenta | El sexto intento en un minuto devuelve 429 |
| Sesiones | Cookie __Host- con Secure, HttpOnly y SameSite; ID rotado al iniciar sesión | Atributos de la cookie visibles en DevTools; la sesión antigua deja de ser válida tras el inicio de sesión |
| Entradas | Consultas parametrizadas en todas partes; salida codificada según el contexto; archivos subidos validados por su contenido | Ninguna consulta construida con cadenas en una búsqueda por el código; los payloads XSS se muestran como texto |
| Control de acceso | Enrutamiento con denegación por defecto; comprobaciones de propiedad por objeto; CORS estricto | Repetir peticiones con los IDs de otro usuario devuelve 404 o 403 |
| Dependencias | Lockfile en el repositorio; npm ci; auditoría en CI; actions fijadas a un SHA; SRI en los scripts de CDN | La compilación falla ante un CVE de gravedad alta |
| Servidor | Solo 80 y 443 públicos; SSH solo con claves; actualizaciones de seguridad desatendidas; secretos en variables de entorno o en un vault | El Verificador de Puertos muestra 22, 3306 y 6379 cerrados desde internet |
| Copias de seguridad | 3-2-1 con una copia inmutable; restauración probada | Última prueba de restauración correcta realizada hace menos de 90 días |
| Correo | SPF -all, DKIM, DMARC p=reject, MTA-STS | El Verificador DMARC no muestra errores; llegan los informes agregados |
| Monitorización | Eventos de autenticación y autorización registrados de forma centralizada; alertas sobre patrones; monitorización externa de disponibilidad, certificados y DNS | Alerta de prueba enviada y recibida |
| Respuesta | Plan de incidentes por escrito; security.txt publicado; ejercicio de simulación realizado | Plan revisado en los últimos 12 meses |
Cómo encaja con el OWASP Top 10:2025
El OWASP Top 10 es la lista de riesgos de aplicaciones web más citada, y su edición de 2025 reordenó los puestos e introdujo dos categorías nuevas. Si un cliente, un auditor o un marco de cumplimiento te pregunta cómo lo abordas, esta es la correspondencia con las prácticas de esta guía:
| OWASP Top 10:2025 | Prácticas de esta guía |
|---|---|
| A01 Broken Access Control (control de acceso roto) | Denegación por defecto, comprobaciones por objeto, CORS estricto, paneles de administración blindados |
| A02 Security Misconfiguration (configuración de seguridad incorrecta) | Cabeceras de seguridad, HSTS, puertos cerrados, cabeceras de versión eliminadas, listado de directorios desactivado |
| A03 Software Supply Chain Failures (fallos en la cadena de suministro de software; nueva) | Lockfiles, auditorías, actions fijadas, SRI, SBOM, CI con mínimo privilegio |
| A04 Cryptographic Failures (fallos criptográficos) | TLS 1.2+ y 1.3, HSTS, Argon2id, gestión de secretos, copias de seguridad cifradas |
| A05 Injection (inyección) | Consultas parametrizadas, codificación de salida, CSP, validación de archivos subidos |
| A06 Insecure Design (diseño inseguro) | Modelo de amenazas por capa, valores por defecto que fallan en cerrado, rate limiting desde el diseño |
| A07 Authentication Failures (fallos de autenticación) | Reglas de contraseñas del NIST, comprobación de filtraciones, MFA y passkeys, rotación de sesiones |
| A08 Software or Data Integrity Failures (fallos de integridad del software o de los datos) | SRI, dependencias firmadas y fijadas, deserialización segura |
| A09 Security Logging and Alerting Failures (fallos de registro y alertas de seguridad) | Registro centralizado, alertas por patrones, monitorización externa |
| A10 Mishandling of Exceptional Conditions (gestión incorrecta de condiciones excepcionales; nueva) | Fallar en cerrado, respuestas de error uniformes, ninguna traza de pila visible para los usuarios |
Comprueba las cabeceras de seguridad de tu sitio en segundos
El Verificador de Cabeceras HTTP gratuito de DNS Robot analiza cualquier URL y califica sus cabeceras de seguridad de la A a la F: HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy y Permissions-Policy, con el valor exacto que encontró y lo que falta. Sin registro.
Probar el Verificador de Cabeceras HTTPAdvertisement
Preguntas frecuentes sobre seguridad web
Fuerza HTTPS con TLS moderno y HSTS, envía cabeceras de seguridad estrictas (sobre todo una Content Security Policy), aplica hash a las contraseñas con Argon2id respaldado por MFA y rate limiting, usa consultas parametrizadas y codificación de salida, aplica el control de acceso en el servidor para cada objeto, mantén las dependencias parcheadas y fijadas, cierra los puertos que no uses y registra los eventos de seguridad de forma centralizada. Protege primero el dominio y el DNS, porque todo lo demás depende de que sigas controlando tu nombre.