Curso exprés · No. 22
Casi toda vulnerabilidad web se remonta a un mismo error: confiar en datos que vinieron de fuera. El atacante controla la petición, el formulario, la URL, el archivo subido — todo. La seguridad web es la disciplina de tratar cada fragmento de entrada externa como hostil hasta que demuestre ser seguro, y de aprender el puñado de ataques clásicos que castigan a quien lo olvida.
Solo lo esencial · Una imagen por idea · Aprende las palabras
Antes de cualquier ataque concreto, hay un único principio que previene la mayoría de ellos. Interioriza esta idea y el resto del curso es solo eso, aplicado en distintos lugares.
Toda entrada externa está controlada por el atacante
Un puesto de control fronterizo trata cada paquete que llega como potencialmente peligroso hasta inspeccionarlo — no deja pasar las cosas solo porque parezcan corrientes.
Todo lo que entra en tu sistema desde fuera — campos de formulario, URLs, cabeceras, archivos subidos, llamadas a la API — lo controla quien lo envió, y eso podría ser un atacante. Pueden enviar cualquier cosa, no solo lo que tu formulario pretendía. Así que la regla fundamental es: trata toda entrada externa como hostil hasta que la hayas validado. Casi toda vulnerabilidad de este curso es, en el fondo, un lugar donde alguien confió en una entrada en la que no debía.
El trust boundary es donde viven las comprobaciones
El muro alrededor de un edificio, con una sola puerta vigilada — dentro se confía, fuera no, y todo lo que cruza hacia adentro se revisa en esa línea.
Un trust boundary es la línea entre el mundo exterior y tu sistema. Los datos que cruzan hacia adentro deben comprobarse en esa frontera, porque una vez dentro, tu código tiende a tratarlos como seguros. El error clásico es validar en el cliente (el navegador) y suponer que basta — pero el atacante se salta el navegador por completo. Las comprobaciones de seguridad reales ocurren en el servidor, en la frontera que tú de verdad controlas.
Valida la entrada, escapa la salida
Un portero comprueba los documentos en la puerta (quién entra), y un traductor se asegura de que tus palabras no puedan malinterpretarse como una orden en la siguiente sala (cómo salen las cosas). Dos tareas distintas.
Dos hábitos detienen la mayoría de los ataques. Valida la entrada: rechaza los datos que no tengan la forma que esperas — tipo incorrecto, demasiado largos, malformados. Escapa la salida: cuando pones datos en otro contexto (una consulta a la base de datos, una página HTML, un comando de shell), neutraliza cualquier cosa que pudiera leerse como código allí. Casi todos los ataques de abajo son entrada no confiable que se cuela en un lugar donde se interpreta como instrucciones en vez de como datos.
Cada fragmento de entrada externa está controlado por el atacante. Valídalo en la frontera, en el servidor, y escápalo en todos los lugares donde se use. Esa única regla previene la mayoría de los ataques.
La clase de ataque más dañina es también la más sencilla de entender: la entrada no confiable se trata como código. El caso de manual es SQL injection, y muestra el patrón entero.
Injection: la entrada se vuelve código
Un formulario que pide tu nombre, donde alguien escribe una instrucción en su lugar — y el empleado, al leer la línea entera en voz alta, ejecuta la instrucción sin querer.
La injection ocurre cuando entrada no confiable se mezcla en un comando o consulta y se ejecuta como parte de él. El sistema pretendía usar tu entrada como datos, pero la entrada se diseñó para leerse como código. Es la misma falla de raíz en SQL, en comandos de shell y más: la frontera entre «la instrucción» y «los datos del usuario» nunca se hizo cumplir, así que el atacante suministró instrucciones.
SQL injection, el caso clásico
Una papeleta de petición de biblioteca donde, en el campo del título, alguien escribe «...y además abre todas las puertas» — y el sistema automatizado obedientemente hace ambas cosas.
En la SQL injection, un atacante teclea sintaxis de base de datos en un campo normal. Si tu código construye una consulta pegando la entrada dentro de una cadena, su entrada pasa a formar parte de la consulta — lo que les permite leer datos de otros usuarios, saltarse un inicio de sesión o borrar tablas. Ha sido una de las vulnerabilidades web más comunes y graves durante décadas, y nace por completo de construir consultas concatenando cadenas con entrada no confiable.
Las parameterized queries son la solución
Un formulario con cajas etiquetadas y cerradas: lo que escribas en la caja «nombre» se trata solo como un nombre, nunca como parte de las instrucciones del formulario, pongas lo que pongas.
La cura es una parameterized query (prepared statement): escribes la consulta con marcadores de posición y pasas la entrada del usuario por separado, de modo que la base de datos siempre la trata como datos puros, nunca como SQL. La entrada podría ser la cadena más maliciosa imaginable y aun así no puede cambiar la estructura de la consulta. Nunca construyas una consulta concatenando entrada en una cadena — usa parámetros, siempre, y la SQL injection sencillamente no puede ocurrir.
La injection es entrada no confiable ejecutada como código. La SQL injection es el caso clásico, y las parameterized queries — entrada pasada como datos, nunca concatenada — son la cura.
Si la injection es entrada que se vuelve código en el servidor, el cross-site scripting es entrada que se vuelve código en el navegador. Convierte tu propia página en un arma contra tus usuarios.
XSS: la entrada se vuelve script en el navegador
Un tablón de anuncios comunitario donde alguien clava una nota amañada para que a quien la lea le vacíen los bolsillos en silencio — el peligro lo entrega el propio tablón de confianza.
El cross-site scripting (XSS) es cuando un atacante logra que su JavaScript se ejecute en el navegador de otro usuario, en tu sitio. Si tomas la entrada del usuario — un comentario, un nombre, un perfil — y la pones directamente en una página, un atacante puede enviar un <script> en vez de texto, y se ejecuta para todo el que vea esa página. Su código actúa ahora con la sesión de tu usuario: roba cookies, lo suplanta, desfigura la página.
Se ejecuta con la confianza de tu usuario
Un impostor vestido con un uniforme de confianza: la gente obedece porque el uniforme — el dominio de tu sitio — es lo que confían, no la persona dentro de él.
El XSS es peligroso porque el script malicioso se ejecuta como tu sitio, con toda la confianza que el usuario da a tu dominio. Puede leer lo que el usuario ve, actuar como él y enviar sus datos al atacante. El navegador no tiene forma de saber que el script no era tuyo — vino de tu página. Por eso «es solo un campo de comentarios» son célebres últimas palabras: cualquier punto donde la entrada del usuario llegue a la página es un posible agujero de XSS.
Escapa la salida y usa una content policy
Un traductor que muestra las palabras de cada invitado como texto plano en la pantalla — así, aunque alguien grite una orden, aparece como palabras entrecomilladas inofensivas, no como un mandato que nadie acata.
La solución central es escapar la salida: cuando pones datos del usuario en HTML, convierte caracteres como < y > en texto de visualización inofensivo para que se muestren como contenido, nunca se ejecuten como markup. Los frameworks modernos lo hacen por defecto, razón por la cual deberías dejarles hacerlo. Súmale una Content Security Policy — una regla del navegador que restringe qué scripts pueden ejecutarse — como segunda línea de defensa. Trata como no confiable todo lo que llega a la página, y neutralízalo a la salida.
El XSS es entrada no confiable que se ejecuta como script en los navegadores de tus usuarios, con la confianza de tu sitio. Escapa todo lo que llega a la página, y añade una Content Security Policy.
El siguiente ataque no inyecta nada — abusa de la propia costumbre del navegador de adjuntar tus credenciales a cada petición, engañándolo para actuar en tu nombre sin tu intención.
CSRF: engañan a tu navegador para que actúe
Alguien falsifica tu firma en un formulario y lo envía desde tu dirección — el banco ve una petición válida y firmada de tu parte y la procesa, sin saber nunca que no la escribiste tú.
La cross-site request forgery (CSRF) explota el hecho de que los navegadores adjuntan automáticamente tus cookies — incluida tu sesión iniciada — a cualquier petición a un sitio. Un atacante coloca una petición oculta a tu banco en su propia página maliciosa; cuando la visitas mientras estás con la sesión iniciada en el banco, tu navegador envía la petición con tu sesión adjunta, y el banco cree que lo quisiste. Te hacen actuar sin que jamás lo hayas elegido.
El problema del confused deputy
Un asistente de confianza con las llaves, engañado por una nota falsificada para abrir la cámara acorazada — el asistente tenía la autoridad y fue embaucado para usarla por cuenta de otro.
El CSRF es un ataque de «confused deputy» (delegado confundido): tu navegador posee autoridad real (tu sesión) y es engañado para ejercerla a favor del atacante. El servidor no puede notar la diferencia, porque la petición se ve exactamente como una genuina — mismas cookies, mismo usuario. La falla no son credenciales robadas; es que una petición válida fue disparada por alguien que no eres tú, y nada demostró tu intención.
Demuestra la intención con tokens y SameSite
Un banco que exige un código de un solo uso, impreso únicamente en tu propio extracto, con cada transferencia — una petición falsificada desde otro lugar no puede incluirlo, así que se rechaza.
La solución es exigir una prueba de que tu página hizo la petición. Un CSRF token es un valor secreto que tu sitio incrusta en sus propios formularios y comprueba al enviarlos; la página de un atacante no puede conocerlo, así que las peticiones falsificadas fallan. El complemento moderno es el atributo de cookie SameSite, que le dice al navegador que no envíe tu cookie de sesión en peticiones que vienen de otros sitios. Juntos garantizan que una acción sensible vino de tu sitio, con tu intención.
El CSRF engaña a tu navegador para que dispare una petición con tu sesión adjunta. Demuestra la intención con un CSRF token y cookies SameSite, para que las peticiones cross-site falsificadas se rechacen.
Algunos datos son tan sensibles que cómo los guardas es en sí mismo una decisión de seguridad. Las contraseñas son el ejemplo clásico, y revelan la diferencia entre dos palabras que la gente confunde constantemente.
Nunca guardes contraseñas como texto plano
Guardar una lista de las llaves de casa de todos en un cajón sin cerrar en recepción — un solo robo, y todos los hogares quedan abiertos. La comodidad es la catástrofe.
Si guardas las contraseñas como texto legible, entonces cualquiera que consiga tu base de datos — por una brecha, una filtración, alguien de dentro — tiene al instante la contraseña de cada usuario. Y como la gente reutiliza contraseñas, también has comprometido sus otras cuentas. Guardar una contraseña como texto plano está entre los errores más graves y básicos de la seguridad web. Esos datos son demasiado peligrosos para mantenerlos en una forma que puedas leer.
El hashing es de un solo sentido; la encryption es de dos
Una trituradora de papel frente a una caja cerrada: la caja puede abrirse y devolver el original, pero la trituradora convierte el papel en confeti que jamás podrás recomponer — y, sin embargo, el mismo papel siempre se tritura igual.
Esta es la distinción que hay que clavar. La encryption es reversible: con la clave, conviertes los datos cifrados de vuelta en el original (úsala para datos que debas volver a leer, como una API key guardada). El hashing es de un solo sentido: convierte la entrada en una huella de tamaño fijo que no puede revertirse de vuelta a la entrada, pero la misma entrada siempre produce la misma huella. Resuelven problemas distintos, y confundirlos es un error clásico.
Haz hash de las contraseñas, con un salt
Comprobar un sello de cera: no necesitas la carta original para verificarlo — solo vuelves a estampar y comparas las impresiones. Confirmas una coincidencia sin guardar nunca el secreto en sí.
Haces hash de las contraseñas porque en realidad nunca necesitas la contraseña de vuelta — solo necesitas comprobarla. Guarda el hash; en el inicio de sesión, haz hash de lo que tecleen y compara. Una brecha filtra huellas, no contraseñas. Añade un salt — un valor aleatorio único por contraseña — para que contraseñas idénticas obtengan hashes distintos y las tablas de ataque precomputadas fallen. Usa un hash de contraseña lento y de propósito específico (como bcrypt o Argon2), nunca uno general y rápido, para que adivinar salga caro.
Nunca guardes contraseñas como texto plano. La encryption es reversible para datos que debas volver a leer; el hashing es de un solo sentido para datos que solo verificas — haz hash de las contraseñas, con salt, con un algoritmo lento.
Más allá de los ataques concretos hay dos principios que limitan el daño cuando algo se cuela — porque en seguridad asumes que algo, tarde o temprano, lo hará.
Da a cada parte el least privilege que necesite
Una tarjeta llave de hotel que solo abre tu habitación y el gimnasio — no todas las puertas del edificio. Si se pierde, el daño queda contenido a lo que podía alcanzar.
El least privilege significa que cada usuario, servicio y credencial recibe solo el acceso mínimo necesario para su trabajo — nada más. La cuenta de base de datos que usa tu aplicación web no debería poder borrar tablas si solo lee y escribe filas. Entonces, cuando algo se vea comprometido — y asume que ocurrirá — el radio de impacto es pequeño, acotado por lo que esa pieza tenía permitido hacer. Los permisos demasiado amplios convierten una brecha pequeña en una total.
Defense in depth: capas, no un único muro
Un castillo con foso, muro, puerta y guardias — no se confía en que una sola barrera sea perfecta, así que cada una atrapa lo que la anterior dejó pasar.
El defense in depth significa superponer protecciones independientes para que un fallo no sea fatal. Valida la entrada y usa parameterized queries y ejecuta con least privilege y escapa la salida. Cualquier control aislado puede fallar o saltarse; juntos hacen que un compromiso total exija vencerlos todos a la vez. La seguridad no es un único muro perfecto — son controles corrientes que se solapan, cada uno asumiendo que los demás podrían fallar.
No filtres pistas en errores y respuestas
Una puerta cerrada que, al sacudirla, anuncia servicial «llave equivocada — la verdadera es de latón y está un poco doblada» — diciéndole al intruso exactamente cómo entrar.
Los errores verbosos y las respuestas que comparten de más le entregan al atacante un mapa. Un inicio de sesión que dice «contraseña incorrecta» (frente a «no existe ese usuario») confirma qué cuentas existen; una traza de pila expone tu framework, las versiones y la estructura de las consultas. Muestra a los usuarios un mensaje genérico, registra el detalle en privado, y nunca devuelvas más datos de los que el cliente necesita. El fallo silencioso no es solo pulcro — le niega al atacante el reconocimiento que hace fácil el siguiente paso.
Asume que algo se colará. El least privilege acota el daño, el defense in depth hace que ningún fallo aislado sea fatal, y los errores silenciosos le niegan al atacante el mapa.
La seguridad es una práctica tejida en cómo construyes, no una función atornillada al final. Los hábitos son pocos, y la mayoría son la única regla aplicada con disciplina.
Apóyate en el framework y mantenlo parcheado
No forjas tus propias cerraduras — montas las probadas y las reemplazas cuando se anuncia un fallo. Hacerte la tuya es como acabas con una cerradura peor.
La mayoría de los ataques clásicos ya están resueltos por frameworks y librerías maduros: escapan la salida, parametrizan las consultas y manejan los CSRF tokens por ti — si los usas como están pensados en vez de buscarles la vuelta. Y como constantemente se encuentran vulnerabilidades en las dependencias, mantenlas actualizadas; una librería sin parchear es una de las vías de entrada más comunes. No inventes tu propia criptografía ni tu propio escapado — usa las herramientas contrastadas y mantenlas al día.
Conoce el OWASP Top 10 como tu mapa
Una lista de comprobación previa al vuelo existe porque los mismos pocos fallos causan la mayoría de los accidentes — los revisas cada vez en vez de redescubrirlos en el aire.
No tienes que imaginar cada amenaza. El OWASP Top 10 es la lista del sector de las vulnerabilidades web más comunes y graves — injection, control de acceso roto y las demás — y funciona también como lista de comprobación. Repasa tu aplicación contra él y atraparás las categorías que causan la abrumadora mayoría de las brechas reales. Usar un mapa conocido vence a esperar que se te ocurriera todo por tu cuenta.
- ¿Por dónde entra la entrada externa, y se valida en la frontera en el servidor? - Consultas — ¿parameterized, nunca construidas concatenando entrada? - Salida a la página — ¿escapada, para que la entrada del usuario no pueda ejecutarse como script? - Acciones que cambian estado — ¿protegidas contra CSRF con tokens y SameSite? - Datos sensibles — ¿contraseñas con hash y un salt, secretos nunca en texto plano? - Least privilege — ¿tiene cada parte solo el acceso que necesita?
- trust boundary / validar la entrada / escapar la salida — la única regla y sus dos hábitos.
- injection / SQL injection / parameterized query — entrada ejecutada como código, y la cura.
- XSS / Content Security Policy — entrada ejecutada como script en el navegador, y sus defensas. - CSRF / CSRF token / SameSite — una petición falsificada con tu sesión, y cómo demostrar la intención. - hashing / encryption / salt — verificar de un solo sentido frente a leer de dos, y aleatoriedad por contraseña. - least privilege / defense in depth — acota el daño, y superpón las protecciones. - OWASP Top 10 — el mapa del sector de las vulnerabilidades más comunes.
- Tratas toda entrada externa como hostil, comprobada en el servidor en la frontera. - Las consultas son parameterized y la salida de la página está escapada — por defecto, mediante el framework. - Las peticiones que cambian estado llevan un CSRF token, y las cookies son SameSite. - Las contraseñas tienen hash y salt; nada sensible se guarda en texto plano. - Cada parte se ejecuta con least privilege, las dependencias se mantienen parcheadas, y compruebas contra el OWASP Top 10.
La seguridad web es en su mayoría una única regla aplicada con disciplina: nunca confíes en la entrada. Valida en la frontera, escapa a la salida, guarda los secretos a salvo, y superpón defensas para cuando algo se cuele.