Cómo proteger una página web: guía por capas para una aplicación con usuarios
Las diez capas que protegen una aplicación web con usuarios, con un ejemplo en Express y cómo comprobar cada una.
- 25 min de lectura
Una página web con usuarios tiene un frontend, una API y un inicio de sesión. A partir de aquí se llama aplicación web.
La edición 2025 del OWASP Top 10 sitúa el control de acceso roto en primer lugar y la configuración de seguridad incorrecta en segundo1. Varios fallos de configuración aparecen ya en el primer borrador de una aplicación Express. Sin ajustes, responde con X-Powered-By: Express, crea la cookie de sesión sin atributos de seguridad y devuelve la traza de un error al navegador.
Ningún control protege la aplicación entera. Cada capa evita una amenaza concreta, y las demás capas cubren lo que esa deja pasar. Las diez capas siguen en orden, y cada una enlaza al artículo que profundiza en ella. El ejemplo usa Node.js y Express, pero las capas y las comprobaciones con curl valen para cualquier framework.
Las mediciones se hicieron el 27 de septiembre de 2026 con Node.js 24.19.0, Express 5.2.1, helmet 8.3.0, express-rate-limit 8.7.0, curl 8.12.1 y Chrome 153.0.8010.54. Todo el código está reconstruido para el artículo.
1. Qué capas tiene una aplicación web y qué protege cada una
| Capa | Qué evita | Control principal | Para profundizar |
|---|---|---|---|
| 1. HTTPS | Que alguien lea o cambie el tráfico | HTTPS y HSTS | Punto 3 |
| 2. Cabeceras de seguridad | Scripts inyectados y la carga en un iframe ajeno |
CSP, frame-ancestors y nosniff |
Punto 4 |
| 3. Sesión y cookies | El robo de la sesión | HttpOnly, Secure y SameSite |
Punto 5 |
| 4. Contraseñas e intentos | El robo de contraseñas y la fuerza bruta | Un hash lento y un límite por cuenta | Punto 6 |
| 5. Permisos | Leer o cambiar datos ajenos | La comprobación en el servidor | Ocho cosas que revisar, puntos 1 y 2 |
| 6. Entrada y salida | La inyección y el XSS | Consultas parametrizadas y salida escapada | Ocho cosas que revisar, puntos 3, 5 y 8 |
| 7. Otros orígenes (CORS y CSRF) | Que otra web lea la API o actúe con la sesión | Lista de orígenes y token anti-CSRF | Qué es CORS |
| 8. Secretos | Claves expuestas en el repositorio o en el navegador | Secretos fuera del código y del bundle | Ocho cosas que revisar, punto 4 |
| 9. Dependencias | Código de terceros que se ejecuta al instalar | Archivo de bloqueo, firmas y scripts de instalación bloqueados | npm y pnpm |
| 10. Errores y registros | Detalles internos en las respuestas y datos en los registros | Respuesta genérica con un identificador | Ocho cosas que revisar, punto 7 |
Cada capa se explica con lo que pasa sin ella en una aplicación de ejemplo y con la orden que la comprueba. Los artículos enlazados muestran el mismo fallo visto en el código, con su corrección.
2. Cómo preparar el entorno antes de escribir código
Si un agente de programación escribe parte de la aplicación, dos ajustes de su entorno reducen el riesgo desde el principio. El agente no debe poder leer .env, y cada cambio tiene que pasar una revisión. Primeros pasos para programar con IA explica los dos. Cómo pedirle a la IA código seguro explica qué poner en la petición y qué comprobar con el compilador, el linter y las pruebas.
Cómo hacerlo. Antes de pedir la primera función, añada al archivo de instrucciones del proyecto las capas de la tabla del punto 1 que su aplicación necesita.
3. Cómo servir la aplicación por HTTPS y activar HSTS
HTTPS cifra el tráfico entre el navegador y el servidor. HSTS (HTTP Strict Transport Security) indica al navegador que use siempre HTTPS con ese dominio, durante los segundos que marca max-age2.
El servidor solo debe enviar la cabecera por HTTPS2. Si llega por HTTP, el navegador la ignora. Por HTTP, el servidor solo redirige:
// El servidor HTTP solo redirige a HTTPS.
const redirigir = (req, res) => {
res.writeHead(301, { Location: `https://localhost:3443${req.url}` }).end();
};
// HSTS solo por HTTPS: por HTTP no se envía (RFC 6797, 7.2).
if (req.secure) res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
OWASP recomienda un max-age de dos años, 63072000 segundos, e incluye preload en su ejemplo3. La lista de precarga de los navegadores exige al menos un año, includeSubDomains y la directiva preload. Sin embargo, esa misma lista no recomienda la precarga y pide no incluir preload por defecto4. Por eso el ejemplo no la incluye.
Cómo hacerlo.
- Active HTTPS en la plataforma de despliegue o en el proxy.
- Redirija con
301toda petición HTTP a la misma ruta en HTTPS. - Envíe
Strict-Transport-Securitysolo en las respuestas HTTPS. Si un proxy termina HTTPS, fijeapp.set('trust proxy', 1): sin ese ajuste,req.securees falso y la cabecera no se envía.
Cómo comprobarlo. La dirección con http:// responde con la redirección y la dirección con https:// trae la cabecera:
$ curl -sI http://localhost:3000/
HTTP/1.1 301 Moved Permanently
Location: https://localhost:3443/
$ curl -skI https://localhost:3443/
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=63072000; includeSubDomains
4. Qué cabeceras de seguridad enviar
Las cabeceras de seguridad indican al navegador qué puede ejecutar y cargar la página. Sin configuración, la aplicación de ejemplo no envía ninguna, y anuncia el framework que usa:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Junto a HSTS, OWASP recomienda, entre otras, estas tres cabeceras, aquí escritas a mano3:
res.setHeader('Content-Security-Policy', "default-src 'self'; frame-ancestors 'none'; form-action 'self'");
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
Content-Security-Policy(CSP) solo permite cargar recursos del propio origen. Condefault-srcy sin'unsafe-inline', el navegador no ejecuta scripts escritos dentro del HTML5.frame-ancestors 'none'impide que otra web cargue la página en uniframepara engañar al usuario. Sustituye aX-Frame-Options3.X-Content-Type-Options: nosniffimpide que el navegador adivine el tipo de un archivo. No interpreta como HTML ni ejecuta como script algo servido con otro tipo3.Referrer-Policylimita qué parte de la URL se envía a otros sitios.strict-origin-when-cross-originya es el valor por defecto de los navegadores, y la cabecera lo fija de forma explícita.
Además, app.disable('x-powered-by') quita la cabecera que anuncia Express. X-XSS-Protection no se envía: OWASP pide no enviarla o enviarla con 03.
Con la CSP activa, Chrome bloquea el script escrito dentro del HTML de la página de ejemplo:
Executing inline script violates the following Content Security Policy directive
'default-src 'self''. Either the 'unsafe-inline' keyword, a hash
('sha256-Gl02dwIVyohd6RBcZoYZc3TfZvDUQjb/ZiHsJ3xX58U='), or a nonce ('nonce-...') is
required to enable inline execution. Note also that 'script-src' was not explicitly set, so
'default-src' is used as a fallback. The action has been blocked.
Para que un script propio se ejecute, muévalo a un archivo .js o autorícelo con el hash o el nonce que indica el mensaje. No añada 'unsafe-inline', porque anula gran parte de la protección5.
El paquete helmet 8.3.0 configura trece cabeceras con una sola línea, app.use(helmet()): envía doce y quita X-Powered-By6. Tres de sus valores por defecto difieren de los del ejemplo: Referrer-Policy: no-referrer, frame-ancestors 'self' y un max-age de un año. Ajústelos con sus opciones si quiere los del ejemplo.
Cómo comprobarlo. Pida las cabeceras de la página principal:
$ curl -skI https://localhost:3443/
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; form-action 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Powered-By no aparece. Abra cada tipo de página con la consola del navegador abierta y compruebe que la CSP no bloquea ningún script ni ningún estilo propio.
5. Cómo proteger la sesión y las cookies
La cookie de sesión identifica al usuario en cada petición. Quien la obtiene actúa como ese usuario. En el primer borrador, la cookie no lleva atributos de seguridad, y cualquier script de la página la lee con document.cookie. Las dos líneas salen de dos inicios de sesión distintos:
Set-Cookie: sesion=8f81d9b1-ee09-471d-9134-ac51dd2b3c08; Path=/
document.cookie = "sesion=a093f210-c912-4b0b-9afe-23761b9c2a2f"
OWASP considera obligatorios Secure y HttpOnly en la cookie de sesión. Pide además fijar SameSite de forma explícita, preferiblemente Strict, sin depender del valor por defecto del navegador10:
res.cookie('__Host-sesion', id, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' });
El ejemplo usa Lax para que un enlace desde otro sitio llegue con la sesión iniciada. Use Strict si la aplicación no lo necesita.
HttpOnlyimpide que JavaScript lea la cookie. Un XSS ya no puede copiarla11.Securesolo la envía por HTTPS.SameSite=Laxno la envía en peticiones de otros sitios. La excepción es la navegación de nivel superior con un método seguro comoGET, por ejemplo un enlace hacia la aplicación.Stricttampoco la envía en ese caso11.- El prefijo
__Host-exige tres condiciones, o el navegador rechaza la cookie. Tiene que fijarse desde un origen HTTPS, llevarSecureyPath=/, y no llevarDomain11. OWASP lo recomienda para la sesión10.
Con estos atributos, la misma página ya no ve la sesión:
Set-Cookie: __Host-sesion=b6e6ed52-0028-45eb-9308-357f3e8a23dd; Path=/; HttpOnly; Secure; SameSite=Lax
document.cookie = ""
Cómo hacerlo.
- Genere un identificador de sesión nuevo al iniciar sesión, como hace
randomUUID()en el ejemplo. OWASP obliga a renovarlo en cada cambio de privilegio10. - Fije
HttpOnly,Secure,SameSiteyPath=/, y use el prefijo__Host-. - Caduque la sesión en el servidor. OWASP propone de 15 a 30 minutos de inactividad en aplicaciones de riesgo bajo y de 2 a 5 en las de alto valor. Para el límite absoluto, propone de 4 a 8 horas si la aplicación se usa durante una jornada de trabajo10.
- Al cerrar sesión, borre la sesión en el servidor, además de la cookie. Según OWASP, la invalidación en el servidor es la obligatoria10. Si solo se borra la cookie, el identificador sigue siendo válido para quien lo haya copiado.
- Si el frontend y la API están en sitios distintos y necesitan la cookie, lea cómo permitir cookies entre orígenes sin abrir la API.
Cómo comprobarlo. Inicie sesión con curl y lea la cabecera Set-Cookie. Después, con la sesión iniciada en el navegador, escriba document.cookie en la consola: la cookie de sesión no debe aparecer. Por último, guarde la cookie, cierre la sesión y repita una petición con la cookie guardada: la respuesta tiene que ser 401.
6. Cómo guardar las contraseñas y limitar los intentos
Una contraseña se guarda como un hash lento, nunca en claro ni con un hash rápido como SHA-256. OWASP recomienda Argon2id. Si no está disponible, recomienda scrypt; bcrypt, solo en sistemas heredados, y PBKDF2 si se exige FIPS-14012.
Node.js incluye scrypt en node:crypto, sin dependencias. Sus parámetros por defecto (N = 214, r = 8, p = 1) no alcanzan ninguna de las combinaciones mínimas de OWASP1213. El ejemplo usa la primera, N = 217, r = 8 y p = 1:
// Parámetros de scrypt: N = 2^17, r = 8, p = 1.
const PARAMS = { N: 2 ** 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
export async function hashContrasena(contrasena) {
const sal = randomBytes(16);
const hash = await scryptAsync(contrasena, sal, 64, PARAMS);
return `${sal.toString('hex')}:${hash.toString('hex')}`;
}
export async function verificarContrasena(contrasena, guardado) {
const [salHex, hashHex] = guardado.split(':');
const esperado = Buffer.from(hashHex, 'hex');
const hash = await scryptAsync(contrasena, Buffer.from(salHex, 'hex'), esperado.length, PARAMS);
return timingSafeEqual(hash, esperado);
}
Estos parámetros necesitan unos 128 MiB. Con el límite por defecto de scrypt en Node.js (maxmem, 32 MiB), la llamada falla13:
ERR_CRYPTO_INVALID_SCRYPT_PARAMS Invalid scrypt params: error:030000AC:digital envelope
routines::memory limit exceeded
Por eso el ejemplo sube maxmem a 256 MiB. Con esos parámetros, calcular un hash tardó unos 260 ms. timingSafeEqual compara el resultado en un tiempo que no depende de dónde difieren los dos valores13.
Ese coste lo paga también el servidor. OWASP advierte de que un factor de trabajo demasiado alto permite a un atacante agotar la CPU del servidor con peticiones de inicio de sesión12. Con estos parámetros, además, cada cálculo reserva unos 128 MiB. Por eso el límite de intentos que sigue se monta en la ruta de inicio de sesión, delante del cálculo del hash: app.post('/login', limite, iniciarSesion).
El límite por cuenta no acota esa carga cuando se prueban muchas cuentas; la acota el segundo límite, por IP. Mida también cuántos inicios de sesión simultáneos puede atender su servidor antes de fijar los parámetros.
Un hash lento no detiene a quien prueba contraseñas contra el inicio de sesión. Para eso sirve un límite de intentos. OWASP pide asociar el contador de fallos a la cuenta, no a la dirección IP14. skipSuccessfulRequests hace que el límite solo cuente los intentos fallidos:
// Cinco intentos por cuenta cada 15 minutos (OWASP asocia el contador a la cuenta, no a la IP).
const limite = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 5,
standardHeaders: 'draft-8',
legacyHeaders: false,
keyGenerator: (req) => String(req.body?.email ?? '').toLowerCase(),
skipSuccessfulRequests: true
});
Tras un inicio de sesión correcto y cinco contraseñas incorrectas, el sexto intento fallido responde 429, que significa demasiadas peticiones15:
HTTP/1.1 401 Unauthorized RateLimit: "5-in-15min"; r=0; t=899
HTTP/1.1 429 Too Many Requests RateLimit: "5-in-15min"; r=0; t=898 Retry-After: 898
El límite de una cuenta no afecta a las demás: justo después, un intento con otra cuenta respondió 401, no 429.
Por la misma razón, el límite por cuenta no detiene el password spraying, que OWASP define como probar una sola contraseña débil contra muchas cuentas distintas16. Cada cuenta recibe un intento, y ningún contador llega a cinco. OWASP recomienda no depender de un único límite de volumen predecible16. Añada un segundo límite, más alto, sobre los intentos fallidos de cada IP, y revise los registros cuando fallen inicios de sesión en muchas cuentas a la vez.
Quedan dos detalles del ejemplo. Si la petición no trae email, la clave es una cadena vacía y todas esas peticiones comparten un contador, así que rechace antes del límite la petición sin email. Además, el almacén por defecto de express-rate-limit guarda los contadores en la memoria del proceso y no los comparte entre varios procesos o servidores17. Con más de una instancia, use un almacén compartido, como Redis.
Bloquear una cuenta también permite a un atacante bloquear a su dueño. Por eso el límite dura unos minutos, aunque un atacante puede renovarlo. OWASP propone permitir el acceso por la recuperación de contraseña aunque la cuenta esté bloqueada. La mejor defensa contra la mayoría de los ataques a contraseñas, incluida la fuerza bruta, es la autenticación multifactor14.
Cómo comprobarlo. Envíe seis intentos con una contraseña incorrecta y compruebe que el sexto responde 429. Después, busque en la base de datos una contraseña guardada: debe incluir la sal y no parecerse a la contraseña. En el ejemplo tiene la forma sal:hash; con Argon2id empieza por $argon2id$.
7. Cómo comprobar los permisos en el servidor
El control de acceso roto es el primer fallo del OWASP Top 10:20251. Un botón oculto en el frontend no impide la acción, porque cualquiera puede enviar la petición sin la interfaz. La API tiene que comprobar en cada petición quién la hace y si ese usuario puede hacer esa acción sobre ese recurso.
Ocho cosas que revisar en el código que genera la IA muestra los dos fallos más frecuentes en el código, con su corrección. El punto 1 trata el permiso que solo se comprueba en la interfaz. El punto 2 trata el recurso que se carga por id sin comprobar de quién es.
Cómo comprobarlo. Pida una acción de administrador con la sesión de un usuario normal: debe responder 403. Pida un recurso de otra cuenta por su id: debe responder 404.
8. Cómo tratar la entrada y la salida
La entrada es todo lo que envía el usuario, y la salida es lo que la aplicación devuelve al navegador. En la aplicación de ejemplo, la búsqueda devuelve el texto tal cual. Esta URL inyecta una imagen con un atributo onerror:
https://localhost:3443/?q=<img src=x onerror="console.log('XSS ejecutado')">
Sin protección, Chrome ejecuta el código del atributo y la consola muestra XSS ejecutado. Ese código podría leer la sesión o enviar peticiones en nombre del usuario. Esto es un XSS reflejado.
La defensa principal es codificar la salida según el contexto, o dejar que lo haga el framework. La CSP del punto 4 es una capa adicional, no la defensa principal20. En el contenido HTML y en los atributos entre comillas, codificar la salida significa escapar cinco caracteres20:
export function escaparHtml(texto) {
return texto
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
Con la salida escapada, el HTML que recibe el navegador lleva el texto codificado, y la página lo muestra como texto sin ejecutarlo:
Resultados para: <img src=x onerror="console.log('XSS ejecutado')">
La entrada se valida en el servidor, con una lista de lo que se acepta21. La validación no sustituye a las consultas parametrizadas ni a la salida escapada. Ocho cosas que revisar muestra la consulta construida sin concatenar texto (punto 3) y la validación en el endpoint (punto 5).
Cómo comprobarlo. Escriba <img src=x onerror=alert(1)> en cada campo que se muestre después en la página: debe aparecer como texto. Escriba una comilla simple en un campo de búsqueda: no debe producir un error 500.
9. Cómo controlar las peticiones de otros orígenes
Otra web abierta en el navegador del usuario puede enviar peticiones a su API. CORS decide si esa web puede leer la respuesta. Qué es CORS explica cómo configurarlo con una lista de orígenes permitidos, y tiene un simulador para probar cada configuración.
CORS no impide que la petición llegue. Si la API usa cookies, otra web puede enviar un formulario que cambie datos con la sesión del usuario. Eso es CSRF (Cross-Site Request Forgery). OWASP pide usar primero la protección que traiga el framework y, si no la hay, un token anti-CSRF en cada petición que cambie datos22.
Si la aplicación solo admite navegadores modernos, OWASP acepta también la cabecera Sec-Fetch-Site, que el navegador envía con el valor cross-site cuando la petición viene de otro sitio. En ese caso exige comprobar además Origin o Referer, porque algunos navegadores antiguos no envían Sec-Fetch-Site22. SameSite es una capa adicional, y con Lax no protege una acción que cambie datos con una petición GET22.
Cómo comprobarlo. Envíe una petición con un origen ajeno y compruebe que la respuesta no trae Access-Control-Allow-Origin. Después, envíe una petición que cambie datos sin el token: debe fallar.
10. Cómo mantener los secretos fuera del código y del navegador
Un secreto es una clave de API, una contraseña de la base de datos o cualquier valor que da acceso a un servicio. OWASP advierte de que muchos acaban escritos en el código fuente, y recomienda detectarlos antes del commit23.
Cómo hacerlo.
- Deje
.envfuera del repositorio con.gitignore. - Guarde los secretos de producción en el gestor de secretos de la plataforma de despliegue. En contenedores, OWASP no recomienda pasarlos por variables de entorno si hay otro método23.
- Añada un detector de secretos al hook de pre-commit y a la integración continua.
- Revise que ninguna clave privada llegue al JavaScript del navegador. Ocho cosas que revisar, punto 4, explica cómo ocurre con las variables públicas de Next.js y cómo buscarla en el bundle.
Cómo comprobarlo. git ls-files .env no debe devolver nada. Si un secreto llegó a un commit, revóquelo y genere otro: borrarlo del archivo no lo borra del historial.
11. Cómo controlar las dependencias
Las dependencias son código de terceros que se ejecuta con los mismos permisos que la aplicación. La edición 2025 del OWASP Top 10 las trata en una categoría nueva, los fallos en la cadena de suministro de software. Amplía la de componentes vulnerables y desactualizados de 20211.
Diferencia entre npm y pnpm en la seguridad al instalar mide qué código ejecuta la instalación y cómo configurar npm para bloquearlo. Ocho cosas que revisar, punto 6, explica cómo comprobar que un paquete que propone la IA existe antes de instalarlo.
Cómo comprobarlo. Instale siempre con el archivo de bloqueo y verifique las firmas:
$ npm ci
found 0 vulnerabilities
$ npm audit signatures
71 packages have verified registry signatures
3 packages have verified attestations
12. Cómo responder a los errores sin filtrar datos
Un error sin controlar muestra información que ayuda a un atacante. En el primer borrador, el manejador de errores devuelve err.stack al navegador cuando falla la base de datos:
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Error: connect ECONNREFUSED 10.0.0.12:5432 (usuario tienda_admin)
at file:///C:/…/tienda-segura/src/index.js:71:9
La respuesta revela la IP interna de la base de datos, un nombre de usuario y la ruta del código en el servidor. El manejador de errores por defecto de Express solo oculta la traza si NODE_ENV vale production24. OWASP pide devolver una respuesta genérica y guardar el detalle en el servidor25:
function errorSeguro(err, req, res, _next) {
const id = randomUUID();
console.error(JSON.stringify({ id, ruta: req.path, error: err.message }));
res.status(500).json({ error: 'error interno', id });
}
Con el manejador, el navegador recibe solo un identificador, y el mismo identificador aparece en el registro del servidor junto al detalle:
HTTP/1.1 500 Internal Server Error
{"error":"error interno","id":"09910789-0f1c-4dc8-914b-fe9a471ca607"}
{"id":"09910789-0f1c-4dc8-914b-fe9a471ca607","ruta":"/api/fallo","error":"connect ECONNREFUSED 10.0.0.12:5432 (usuario tienda_admin)"}
El registro tampoco debe guardar contraseñas, identificadores de sesión ni tokens26.
Cómo comprobarlo. Provoque un error, por ejemplo con la base de datos apagada, y compruebe que la respuesta solo trae el identificador. Busque ese identificador en el registro del servidor.
13. Cómo comprobar la aplicación entera
La lista reúne las comprobaciones de los puntos 3 a 12, por capas. Cada capa enlaza a su punto. Las marcas se guardan en este navegador para que pueda volver más tarde.
Lista de comprobación
0 de 24 comprobados
Marque cada punto después de comprobarlo en su aplicación. Sustituya su-dominio.example por el suyo.
14. Cómo auditar su proyecto con un prompt
Un agente de programación puede recorrer las diez capas en su proyecto. Copie el prompt con el botón «Copiar prompt» del panel «Auditar la seguridad» y péguelo en una sesión abierta en la raíz del proyecto. El agente sigue siete fases:
- Reconoce el proyecto sin escribir nada: el framework, cómo autentica y dónde se configura cada capa.
- Diagnostica las diez capas en el orden de la lista y clasifica cada fallo por gravedad.
- Propone y espera. Presenta los fallos y los cambios, y se detiene hasta que usted confirma.
- Corrige capa por capa, con cambios pequeños.
- Añade pruebas y comprueba que fallan antes de la corrección.
- Repite las comprobaciones de la lista y pega las salidas.
- Entrega un informe con lo que cambió, lo que no pudo comprobar y el artículo que profundiza en cada capa pendiente.
El prompt se ejecutó una vez, como demostración, con Claude Code 2.1.283 y la versión anterior del prompt. El proyecto era una tienda de ejemplo en Express a la que le faltaban casi todas las capas. Tardó 10 minutos sin contar la espera de la confirmación: 4 hasta la propuesta y 6 para corregir y comprobar.
En el diagnóstico encontró trece fallos y, durante la corrección, uno más. Entre ellos había una clave de pagos escrita en el código, un pedido ajeno legible por su id y contraseñas con SHA-256. Las 13 pruebas que escribió fallaban antes de las correcciones, y después pasan todas, junto con la que ya había. Lo que dependía del proxy de producción, como el número de saltos de trust proxy, lo anotó como no comprobado.
Una ejecución sobre un proyecto preparado muestra cómo trabaja el prompt, pero no mide cuántos fallos encuentra en un proyecto real. Revise el informe como revisaría el de una persona.
Cómo comprobarlo. git status y git diff muestran solo cambios de configuración y de seguridad y los archivos de prueba nuevos, y las pruebas pasan. Las comprobaciones de la lista del punto 13 dan el resultado esperado.
Qué queda fuera de las diez capas
Las diez capas tratan los fallos más frecuentes de una aplicación con usuarios, pero no todos. El OWASP Top 10:2025 incluye otros que aquí no se desarrollan1:
- Registro y alertas (A09:2025). Guardar los errores no basta: alguien tiene que recibir un aviso cuando se repiten los inicios de sesión fallidos o las respuestas
403. - Peticiones del servidor a direcciones que elige el usuario, o SSRF. En la edición de 2025 forma parte del control de acceso roto, A0127. Afecta a cualquier función que descarga una URL, como una vista previa de enlaces o una importación. El punto 5 de los ataques web más comunes lo reproduce y lo corrige.
- Condiciones excepcionales (A10:2025): errores que dejan la aplicación en un estado abierto, como un
catchque sigue adelante. El punto 12 solo trata la respuesta que recibe el navegador. - Subida de archivos: el tipo, el tamaño, el nombre y el lugar desde el que se sirven. OWASP lo trata en su File Upload Cheat Sheet28.
- Autenticación multifactor: el punto 6 la cita como la mejor defensa contra los ataques a contraseñas, pero no explica cómo implementarla.
- Copias de seguridad y recuperación, que no dependen del código de la aplicación.
Por dónde seguir
Si empieza desde cero, prepare primero el entorno con Primeros pasos para programar con IA. Después, use la tabla del punto 1 para decidir qué capas necesita su aplicación.
Para revisar el código capa por capa, lea Ocho cosas que revisar en el código que genera la IA. Para las peticiones entre orígenes, lea Qué es CORS, y para las dependencias, la diferencia entre npm y pnpm. Los ataques web más comunes muestra catorce ataques contra estas capas, cada uno con la petición que lo aprovecha y su corrección.
Referencias
- OWASP. OWASP Top 10:2025, Introductiontop10.owasp.org↩
- IETF. RFC 6797, HTTP Strict Transport Security (HSTS)rfc-editor.org↩
- OWASP. HTTP Security Response Headers Cheat Sheetcheatsheetseries.owasp.org↩
- Chromium. HSTS Preload List Submissionhstspreload.org↩
- MDN. Content Security Policy (CSP)developer.mozilla.org↩
- helmet. helmet, READMEnpmjs.com↩
- MDN. Content-Security-Policy, style-srcdeveloper.mozilla.org↩
- MDN. Content-Security-Policy-Report-Onlydeveloper.mozilla.org↩
- Next.js. How to set a Content Security Policy (CSP) for your Next.js applicationnextjs.org↩
- OWASP. Session Management Cheat Sheetcheatsheetseries.owasp.org↩
- MDN. Set-Cookiedeveloper.mozilla.org↩
- OWASP. Password Storage Cheat Sheetcheatsheetseries.owasp.org↩
- Node.js. Crypto: crypto.scrypt y crypto.timingSafeEqualnodejs.org↩
- OWASP. Authentication Cheat Sheetcheatsheetseries.owasp.org↩
- IETF. RFC 6585, Additional HTTP Status Codes, 429 Too Many Requestsrfc-editor.org↩
- OWASP. Credential Stuffing Prevention Cheat Sheetcheatsheetseries.owasp.org↩
- express-rate-limit. Data storesexpress-rate-limit.mintlify.app↩
- express-rate-limit. Troubleshooting proxy issuesexpress-rate-limit.mintlify.app↩
- express-rate-limit. Error codes, ERR_ERL_PERMISSIVE_TRUST_PROXYexpress-rate-limit.mintlify.app↩
- OWASP. Cross Site Scripting Prevention Cheat Sheetcheatsheetseries.owasp.org↩
- OWASP. Input Validation Cheat Sheetcheatsheetseries.owasp.org↩
- OWASP. Cross-Site Request Forgery Prevention Cheat Sheetcheatsheetseries.owasp.org↩
- OWASP. Secrets Management Cheat Sheetcheatsheetseries.owasp.org↩
- Express. Error handling, the default error handlerexpressjs.com↩
- OWASP. Error Handling Cheat Sheetcheatsheetseries.owasp.org↩
- OWASP. Logging Cheat Sheetcheatsheetseries.owasp.org↩
- OWASP. A01:2025 Broken Access Controlowasp.org↩
- OWASP. File Upload Cheat Sheetcheatsheetseries.owasp.org↩