Saltar al contenido
Computer Science Fundamentals
Código fuente
← Inicio

Seguridad

100 preguntas

Correctas
0
Incorrectas
0
Sin responder
100
Dificultad
Número de preguntas

100 preguntas en este filtro

Resumen teórico

Imagina tu casa. Tiene puerta de entrada, ventanas, una cerradura, una llave debajo del felpudo y un vecino que a veces pide azúcar prestada. La seguridad es el trabajo de pensar en todo eso: quién puede entrar, qué puede llevarse y qué hacemos para que la persona equivocada no pueda. El software también tiene puertas y llaves. Un sitio web tiene un inicio de sesión (la puerta de entrada), cookies (notitas que dicen "esta persona ya entró") y una base de datos (el armario donde se guarda todo lo valioso).

La mayoría de las intrusiones en internet no son cosa de película, con un hacker tecleando rápido. Ocurren porque un programador confió en algo que debió verificar: un nombre escrito en una casilla, un número en la dirección, un archivo que alguien subió. Esta materia, llamada seguridad de aplicaciones, enseña cómo fallan los programas cuando alguien los usa de una forma que nadie esperaba, para que los construyamos de modo que no fallen.

Todo aquí es defensivo. Para cada falla explicamos por qué ocurre y, siempre, cómo prevenirla. El código que ves es el ejemplo más pequeño necesario para reconocer el problema, y siempre va junto a su corrección. Las ideas siguen las guías de , la referencia estándar del área.

Al final conocerás las principales familias de fallas web (inyección, scripts plantados en páginas, peticiones falsificadas, contraseñas débiles, sesiones robadas, tokens rotos, falta de comprobación de permisos, servidores engañados, subidas inseguras), los hábitos que frenan cada una y lo básico de criptografía y secretos que sostiene todo lo demás.

Cómo funciona la web: las piezas con las que juega un atacante

Antes de estudiar cómo se rompen las cosas, necesitamos saber cómo funciona normalmente un sitio web. Piensa en un restaurante. Tú (el navegador) te sientas a la mesa y escribes un pedido en un papel. El mesero lo lleva a la cocina (el servidor). La cocina cocina y el mesero trae el plato. El papel del pedido se llama , y el plato es la respuesta.

  Navegador                          Servidor
     |                                  |
     |  GET /invoices/1001  + cookie    |
     | -------------------------------> |  busca los datos,
     |                                  |  arma una página
     |  200 OK  + página (HTML)         |
     | <------------------------------- |
     |                                  |
Un viaje de ida y vuelta entre navegador y servidor

Algunas palabras volverán todo el tiempo. Una cookie es una notita que el servidor le pide al navegador que guarde y que muestre de nuevo en cada visita posterior, como la pulsera de un parque acuático: el personal no recuerda tu cara, solo revisa la pulsera. Un origen es la dirección exacta de un sitio: el protocolo (https), el nombre (shop.test) y el puerto. Dos páginas con orígenes distintos son extrañas entre sí, y el navegador lo hace cumplir con la .

PalabraImagen cotidianaQué es
ClienteEl comensal en la mesaEl navegador o la app del usuario. El usuario lo controla por completo
ServidorLa cocinaEl programa que escribimos y protegemos
CookieUna pulseraUna nota que el navegador devuelve automáticamente en cada petición
OrigenUna dirección de calle completaProtocolo + nombre + puerto, por ejemplo https://shop.test:443
Entrada no confiableLa nota de un extrañoTodo lo que viene de fuera: formularios, URLs, cabeceras, archivos e incluso datos leídos de vuelta de la base de datos
↑ Volver al índice

OWASP Top 10, amenazas y buenos hábitos

¿Por dónde empezar? Los médicos tienen una lista de las enfermedades más comunes. Quienes trabajan en seguridad también. El OWASP Top 10 es un documento de concienciación, no es una ley ni un certificado. Agrupa las fallas en diez categorías de riesgo, ordenadas por la frecuencia con que aparecen en datos reales y por una encuesta a profesionales. Es un buen punto de partida para estudiar y revisar, pero no es una lista completa de todas las fallas, y pasar una revisión basada en él no prueba que una aplicación sea segura.

En este resumen usamos la edición de 2021. Su categoría número uno es Broken Access Control (control de acceso roto): una persona con sesión iniciada alcanza cosas que no son suyas. La inyección, que lideraba la edición de 2017, pasó al tercer lugar.

OWASP Top 10, edición 2021
PosiciónCategoríaEn una frase
A01Broken Access ControlUn usuario lee o cambia lo que no debería
A02Cryptographic FailuresLos datos sensibles no se protegen, o se protegen mal
A03InjectionLos datos se mezclan con código (SQL, shell) y se ejecutan como código
A04Insecure DesignEl propio plan carece de un control necesario
A05Security MisconfigurationValores por defecto, funciones abiertas y errores ruidosos olvidados en producción
A06Vulnerable and Outdated ComponentsBibliotecas viejas con fallas conocidas
A07Identification and Authentication FailuresContraseñas débiles, sesiones débiles, inicio de sesión débil
A08Software and Data Integrity FailuresActualizaciones y datos aceptados sin comprobar de dónde vienen
A09Security Logging and Monitoring FailuresNadie nota la intrusión
A10Server-Side Request Forgery (SSRF)Se engaña al servidor para que llame a lugares a los que no debería

Pensar como defensor: el modelado de amenazas

El consiste en responder cuatro preguntas temprano: ¿Qué estamos construyendo? ¿Qué puede salir mal? ¿Qué vamos a hacer al respecto? ¿Hicimos un buen trabajo? Rinde más en la fase de diseño, antes de que exista código, porque cambiar un dibujo es barato y cambiar un sistema terminado es caro. Cuando la arquitectura cambia, lo haces otra vez.

Una lista práctica para "qué puede salir mal" es STRIDE. Cada letra es un tipo de amenaza, y cada una rompe una propiedad que queremos mantener. Saber qué propiedad se rompió indica el tipo de control que se debe usar.

STRIDE
LetraAmenazaPropiedad rotaControl típico
SSpoofing: hacerse pasar por otra personaAutenticidadAutenticación fuerte
TTampering: alterar datosIntegridadFirmas, comprobaciones en el servidor
RRepudiation: negar que se hizo algoNo repudioRegistros y rastros de auditoría
IInformation disclosure: filtrar secretosConfidencialidadCifrado, control de acceso
DDenial of service: hacer que deje de funcionarDisponibilidadLímites, capacidad, filtrado
EElevation of privilege: obtener más derechosAutorizaciónComprobación de permisos, mínimo privilegio

Principios que aparecen en todas partes

  • Mínimo privilegio: cada persona, programa o cuenta de base de datos recibe solo los permisos que su trabajo necesita. Quien cuida la mascota del curso necesita la llave del salón, no la de toda la escuela. Si la app solo lee productos, su cuenta en la base de datos no debe poder borrar tablas.
  • Defensa en profundidad: apila varias protecciones independientes, porque cualquiera puede fallar. Un castillo tiene foso, muralla y guardias.
  • Seguro por defecto y denegar por defecto: la opción segura es la que obtienes cuando no haces nada.
  • Diseño inseguro frente a implementación insegura: un bug es código que no hace lo que decía el plan. El diseño inseguro es código que hace exactamente lo que decía el plan, cuando el plan olvidó un control. Ejemplo: una tienda deja que cualquiera reserve stock ilimitado durante 24 horas sin pagar. Los bots reservan todo. El código no tiene bug; a la regla le falta un límite. La corrección es rediseñar la regla, y la prevención es escribir casos de abuso junto a los casos de uso.
  • Mala configuración de seguridad: el código está bien pero el despliegue no. Cuentas por defecto admin/admin, listado de directorios activado y páginas de error que muestran el stack trace completo son ejemplos clásicos. La cura es un proceso de endurecimiento (hardening) repetible, igual en todos los entornos.
↑ Volver al índice

Criptografía y TLS: códigos secretos que protegen el camino

La es la caja de herramientas detrás de las contraseñas, los tokens y el candado del navegador. No necesitas las matemáticas. Necesitas saber qué herramienta responde a qué pregunta.

Dos formas de cerrar un mensaje

La criptografía simétrica usa la misma clave para cerrar y abrir, como un diario con una llave que tienen los dos amigos. Es muy rápida, pero hay un problema: ¿cómo acuerdan los dos la clave sin que nadie más escuche? La criptografía asimétrica usa un par de claves. La clave pública puede mostrarse a todo el mundo, como la ranura de un buzón donde cualquiera deja cartas. La clave privada se queda en secreto, como la llave que abre el buzón. Resuelve el problema de compartir, pero es mucho más lenta.

AspectoSimétricaAsimétrica
ClavesUna clave secreta compartidaUna clave pública y una clave privada
VelocidadMuy rápidaMucho más lenta
Problema principalCómo compartir la clave con seguridadEl costo, por eso se usa solo un instante
En TLS sirve paraCifrar todos los datos de la conexiónEl handshake: probar quién es el servidor y acordar las claves

Los sistemas reales usan ambas, lo que se llama criptografía híbrida. TLS usa la herramienta asimétrica, lenta, una vez al principio, para acordar un secreto sin ningún secreto previo. Después de eso, todo se protege con la herramienta simétrica, rápida.

Hash no es cifrado

Una función hash es como una máquina de moler carne: entra cualquier cosa, sale una "huella digital" de tamaño fijo, y no se puede volver a convertir la carne. Es de una sola vía. Un cifrado es una cerradura reversible: con la clave recuperas el original. Responden a necesidades distintas, y usar uno en lugar del otro es un error clásico.

Herramienta¿Reversible?Tamaño de la salidaÚsala para
Hash (por ejemplo SHA-256)NoSiempre igualComprobar que un archivo no cambió
Cifrado (por ejemplo AES)Sí, con la claveCerca del tamaño de la entradaMantener datos en secreto
MAC / HMACNoFijoComprobar que un mensaje no fue alterado por quien no tiene la clave
Firma digitalNoFijoProbar quién escribió un mensaje cuando nadie comparte clave

¿Por qué un hash simple no basta para proteger un mensaje en el camino? Porque si alguien en medio cambia el mensaje, puede simplemente calcular un hash nuevo para el mensaje nuevo. El hash no lleva ningún secreto. Un como HMAC mezcla una clave secreta en el hash. Sin la clave, nadie puede producir un valor válido para un mensaje alterado. Cuando las dos partes no comparten clave, una firma digital cumple el mismo papel.

TLS: qué significa realmente el candado

TLS es el protocolo detrás de https. Cerrar la conversación no basta, porque podrías estar cerrándola para la persona equivocada. Por eso el servidor muestra un certificado, un documento firmado por una autoridad de confianza que dice "esta clave pública pertenece a este dominio". El navegador lo valida. ¿Qué prueba eso? Que la clave pública realmente pertenece al dominio que visitas, y que el servidor tiene la clave privada correspondiente. No prueba que el sitio sea honesto. Un sitio estafador puede tener un candado perfectamente válido.

1. Navegador: "hola, quiero shop.test"
2. Servidor  : certificado (clave pública + firma)
              el navegador revisa: ¿es de shop.test?
3. Ambos     : acuerdan claves de sesión nuevas
              (asimétrica, Diffie-Hellman efímero)
4. Desde aquí: cifrado simétrico rápido
              + comprobación de integridad en cada mensaje
TLS en pasos

Hay una propiedad más, muy buena. Supón que alguien graba hoy todo tu tráfico cifrado y, dentro de cinco años, roba la clave privada del servidor. ¿Puede leer las grabaciones antiguas? Con TLS 1.3, no. Esto se llama confidencialidad directa (forward secrecy). Cada conexión crea sus claves con un intercambio Diffie-Hellman , con números aleatorios que se descartan al final. La clave del certificado se usa solo para probar identidad, no para proteger el tráfico, así que robarla después no abre nada del pasado.

↑ Volver al índice

Inyección: cuando los datos se vuelven comandos

Imagina que le dices a un mesero robot: "Tráeme la sopa". Ahora un niño travieso escribe en la comanda: "sopa, y también abre la caja registradora". Si el robot lee la comanda como una lista larga de instrucciones, obedece las dos. Eso es inyección: el programa debería tratar lo que escribió el usuario como simples datos, pero deja que esos datos se lean como código. Los dos se mezclaron en un mismo trozo de texto.

En la inyección SQL el "robot" es la base de datos. El programa arma la consulta pegando textos, así que un valor con una comilla puede cambiar la forma del comando. El problema es el pegamento, no el usuario.

const email = body.email; // viene del usuario
const sql = "SELECT id, name FROM users WHERE email = '" + email + "'";
const result = await db.query(sql);
// un email como  x' OR '1'='1  cambia el significado de la consulta
Vulnerable: el valor pasa a formar parte del texto SQL
const sql = "SELECT id, name FROM users WHERE email = $1";
const result = await db.query(sql, [email]);
// la base de datos fija primero la forma del comando,
// y solo después recibe email como valor
Corregido: la consulta parametrizada mantiene separados el texto y los valores

¿Por qué funciona la segunda versión? La base de datos primero lee el texto con sus marcadores ($1, o ? en otras bases) y decide cuál es el comando. Solo después recibe los valores, y un valor solo puede ocupar una posición de dato. Así, x' OR '1'='1 es apenas un email raro con el que comparar, no una condición. Esto se llama , y es la defensa principal porque elimina la causa.

Otras defensas y cosas que no funcionan

  • La validación (¿el valor tiene la longitud, el tipo y el formato correctos?) es una buena capa extra. Decide si un valor tiene sentido. No reemplaza a los parámetros. Un campo de apellido debe seguir aceptando O'Brien, así que aceptas el apóstrofo y envías el valor como parámetro.
  • Mínimo privilegio: una cuenta de base de datos que solo lee lo que necesita limita el daño si algo se escapa.
  • Las listas de bloqueo y el escape manual (quitar comillas, prohibir palabras) tratan síntomas y son fáciles de esquivar. No cuentes con ellas.
  • Los ORM (bibliotecas que escriben el SQL por ti) parametrizan sus métodos normales, así que reducen mucho el riesgo. Pero el SQL crudo armado con interpolación de cadenas dentro de un ORM es tan inyectable como cualquier otro.
  • Los datos de tu propia base de datos tampoco son confiables. Si un nombre se guardó con seguridad y luego se pegó en otra consulta, esa segunda consulta es vulnerable. Parametrizar es una regla para toda consulta, no solo para lo que acaba de llegar.
  • Los procedimientos almacenados no son magia. Si un procedimiento arma SQL dinámico pegando su argumento en una cadena, es inyectable. Dentro de la base de datos, el SQL dinámico necesita sus propios parámetros (en PostgreSQL, EXECUTE ... USING).

Cuando la estructura debe cambiar

Los marcadores representan valores, no nombres de columnas, nombres de tablas ni ASC/DESC. Supón que una lista puede ordenarse por price o name. No pegues la palabra recibida en la consulta. Usa la palabra solo como clave de una lista fija y toma el nombre real de la columna de esa lista.

const columns: Record<string, string> = { price: "price", name: "name" };
const column = columns[query.sort];
if (!column) return status(400);
const sql = `SELECT id, name FROM products ORDER BY ${column}`;
// el texto sale de nuestra lista, nunca del usuario
Lista de permitidos para una parte estructural

Los parámetros tampoco cambian lo que un valor significa dentro de un mini-lenguaje propio. En LIKE '%' || $1 || '%', si el usuario envía %, no hay inyección, pero % sigue funcionando como el comodín "cualquier cosa" y coincide con todas las filas. Si la búsqueda debe ser literal, escapa % y _ en el valor.

La misma idea con el sistema operativo

La inyección de comandos es el mismo error con otro intérprete: el shell. Un servidor que arma el texto ping -c 1 más un host escrito por el usuario y se lo entrega a un shell está en problemas, porque caracteres como ;, | y && significan "ejecuta otro comando" para un shell. La cura: no uses el shell. Inicia el programa directamente, indicando el ejecutable y una lista de argumentos, para que cada argumento llegue como dato, y valida el host contra un formato estricto.

// Vulnerable: un shell lee la cadena completa
exec("ping -c 1 " + host);

// Corregido: sin shell, argumentos en lista, host validado antes
if (!/^[a-zA-Z0-9.-]{1,253}$/.test(host)) return status(400);
spawn("ping", ["-c", "1", host]);
Vulnerable y corregido
↑ Volver al índice

Cross-site scripting (XSS): el script de un extraño en tu página

La inyección tiene una prima en la que el "robot" es el navegador. Un navegador lee texto HTML y construye la página a partir de él. Algunas partes del HTML son código, como un bloque <script> o un atributo como onerror="...". En el cross-site scripting (XSS), un script escrito por otra persona termina dentro de tu página, y el navegador no puede saber que no lo escribiste tú. Se ejecuta con el origen del sitio vulnerable, así que ve la página, lee lo que la página puede leer y actúa en nombre de quien la tiene abierta.

La causa raíz es siempre la misma: se escribieron datos en un lugar que se interpreta como código sin codificarlos para ese lugar. Hay tres tipos clásicos, y la diferencia es solo cómo llegan los datos a la página.

TipoCómo llegan los datosEjemplo
Almacenado (stored)El servidor lo guarda y lo sirve a todos los que abren la páginaUn comentario con una etiqueta de script, mostrado a cada lector del artículo
Reflejado (reflected)El servidor devuelve de inmediato algo que vino en la peticiónUna página de búsqueda que imprime tu texto buscado sin cambios
Basado en DOMEl JavaScript de la propia página lee los datos y los inserta en la página. El servidor puede no verlos nuncaUn script que pone en la página lo que va después del # en la dirección

Defensa 1: codificar en la salida

Cuando escribas datos del usuario en HTML, convierte los caracteres de significado especial en equivalentes inofensivos: < pasa a &lt;, > pasa a &gt;, & pasa a &amp;, y las comillas también se reemplazan. El navegador entonces muestra los caracteres como texto en vez de leerlos como etiquetas. No se borra nada; el visitante ve exactamente lo que escribió.

Sin codificación:
  el servidor escribe : <span> <script>bad()</script> </span>
  el navegador ve     : una etiqueta script -> SE EJECUTA

Con codificación:
  el servidor escribe : <span> &lt;script&gt;bad()&lt;/script&gt; </span>
  el navegador ve     : texto plano         -> se muestra, nunca se ejecuta
Por qué la codificación detiene el script

Codifica en el momento de la salida, no al guardar en la base de datos. La codificación correcta depende de dónde cae el dato (texto HTML, atributo, URL, JavaScript, CSS), y eso solo lo sabes al escribir la página. Un dato guardado ya codificado para HTML se vería mal en una API JSON y seguiría siendo inseguro dentro de JavaScript. El contexto importa: algo inofensivo en un lugar puede ser código en otro. En un href o src, por ejemplo, también hay que comprobar el esquema de la URL, porque un enlace javascript: ejecuta código al hacer clic. Acepta solo http: y https:, de una lista de permitidos.

Defensa 2: APIs seguras del navegador

En el XSS basado en DOM el servidor no tiene nada que codificar, porque la parte peligrosa nunca viaja hasta él. La corrección está dentro del script: usa APIs que traten el valor como texto.

const name = decodeURIComponent(location.hash.slice(1));

// Vulnerable: innerHTML interpreta el texto como HTML
// document.querySelector('#greeting').innerHTML = 'Hola, ' + name;

// Corregido: textContent siempre inserta texto plano
document.querySelector('#greeting').textContent = 'Hola, ' + name;
Un XSS basado en DOM y su corrección
Seguro: trata el valor como textoPeligroso: interpreta el valor como marcado
textContent, createTextNodeinnerHTML, outerHTML
setAttribute para atributos que no ejecutan códigodocument.write, insertAdjacentHTML

Frameworks, sanitizadores y lo que queda

  • Los frameworks modernos de interfaz codifican automáticamente los valores que interpolas en las plantillas. El XSS vuelve cuando usas su salida de "HTML crudo" con datos del usuario, pones una URL del usuario en un href o tocas el DOM directamente.
  • Si los usuarios deben poder enviar formato (negrita, listas, enlaces), la codificación lo arruinaría. Pasa el HTML por un sanitizador mantenido que conserve solo una lista de permitidos de etiquetas, atributos y esquemas de URL. Las listas de permitidos fallan de forma segura: todo lo desconocido se elimina. No escribas tú un sanitizador; son muy difíciles de hacer bien.
  • Las cookies HttpOnly son una mitigación, no una corrección. El script no puede leer esa cookie, pero aún puede enviar peticiones como la víctima, porque el navegador adjunta la cookie por sí solo. La corrección sigue siendo la codificación y los destinos seguros, con la Content Security Policy como capa extra (siguiente sección).
↑ Volver al índice

Content Security Policy y cabeceras de seguridad

La codificación es la defensa principal contra el XSS, pero los programadores olvidan cosas. Por eso añadimos una red de seguridad: le decimos al navegador, de antemano, qué puede hacer la página. Piensa en una excursión escolar: la profesora le entrega al conductor del autobús una lista escrita de los lugares que el grupo puede visitar. Si un extraño intenta añadir una parada, el conductor se niega. Esa lista es una Content Security Policy (CSP).

La CSP es una cabecera HTTP del servidor. Es una lista de permitidos aplicada por el navegador: dice de qué orígenes puede la página cargar y ejecutar contenido. Reduce el daño cuando la codificación de salida falla en algún punto, pero no la reemplaza.

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
Una política estricta
DirectivaSignificado
script-src 'self'Solo se ejecutan archivos de script del mismo origen. Los bloques <script> en línea y los manejadores en línea como onclick se rechazan
default-src 'self'Todo lo que no se listó (imágenes, estilos, conexiones) solo del mismo origen
object-src 'none'Sin plugins como <object> o <embed>
base-uri 'none'Sin elemento <base> que podría cambiar a dónde apuntan las direcciones relativas
form-action 'self'Los formularios solo pueden enviarse al mismo origen
frame-ancestors 'none'Nadie puede poner esta página dentro de un <iframe>

Con script-src 'self', si un comentario mal codificado cuela un script en línea en la página, el navegador ejecuta /app.js (un archivo del mismo origen) y bloquea el script en línea. Esa es la ganancia principal: la mayoría de las inyecciones parecen marcado dentro de la página. Para aprovecharla, tu JavaScript debe estar en archivos, no en atributos de evento.

No rompas la red

Cuando el código viejo necesita scripts en línea, alguien puede tentarse a escribir script-src 'self' 'unsafe-inline'. La palabra clave se llama así por algo: le devuelve al atacante el camino más fácil, y la política pasa a valer poco contra XSS. Las formas correctas de mantener un script en línea necesario son moverlo a un archivo o autorizarlo individualmente con un nonce o un hash.

Un se pone tanto en la cabecera (script-src 'nonce-R4nd0m') como en el atributo nonce de tus propias etiquetas de script. Un script inyectado no conoce el valor de esa respuesta y se bloquea. Si el valor es fijo en un archivo de configuración, es igual en todas las páginas, cualquiera puede leerlo una vez y ponerlo en su propia etiqueta, y la protección desaparece.

Desplegarla con seguridad

En un sitio grande y viejo, activar una política estricta puede romper páginas que usan algo que olvidaste. El camino seguro es Content-Security-Policy-Report-Only. El navegador evalúa la política sin aplicarla y envía informes de violaciones. Corrige lo que muestren los informes y, cuando estén limpios, mueve la misma política a la cabecera Content-Security-Policy real.

Otras cabeceras de seguridad

CabeceraQué hacePor qué importa
Strict-Transport-Security (HSTS)Le dice al navegador que use solo HTTPS en este dominio durante max-ageSin ella, la primera petición puede ir por HTTP y alguien en el camino puede impedir el salto a HTTPS. Solo cuenta cuando se recibe por HTTPS
X-Content-Type-Options: nosniffEl navegador debe respetar el Content-Type declarado y no adivinarEvita que un archivo subido lleno de HTML se ejecute porque el navegador creyó que era una página
frame-ancestors (en CSP)Declara quién puede enmarcar la páginaDefiende del clickjacking: tu página cargada invisible en el marco de otro sitio para robar clics. 'none' no permite a nadie, 'self' solo a tu propio origen
↑ Volver al índice

CSRF y cookies SameSite: peticiones falsificadas

Imagina una pizzería que entrega a quien tenga tu tarjeta de cliente frecuente. Un bromista no necesita robar tu tarjeta. Solo grita "¡Oye, pizzería, entrega 50 pizzas en esta dirección!" de una forma que hace que tu propio teléfono haga el pedido, y la pizzería ve el número de tu tarjeta y entrega. Eso es cross-site request forgery (CSRF): otro sitio web hace que el navegador de la víctima envíe una petición que el sitio objetivo acepta como propia de la víctima.

Fíjate en lo que el atacante no hace. No lee la respuesta ni roba la contraseña. Solo necesita que se ejecute una acción con los derechos de la víctima, como cambiar un correo o transferir dinero. Por eso el CSRF apunta a peticiones que cambian estado.

Funciona por cómo se comportan las cookies. Una cookie de sesión es una credencial ambiental: el navegador la adjunta por sí solo a toda petición a ese sitio, incluso cuando la petición la inició una página de otro sitio. El servidor recibe una petición perfectamente válida y no puede saber que el usuario no la quiso.

  página evil.test          Navegador            bank.test
      |  form oculto a bank.test   |                    |
      | -------------------------> |                    |
      |                            | POST /transfer     |
      |                            | + cookie bank.test |
      |                            | -----------------> |
      |                            |  servidor: sesión  |
      |                            |  válida, ¡hazlo!   |
Cómo viaja una petición falsificada

Defensa 1: cookies SameSite

El atributo SameSite le dice al navegador cuándo puede enviarse la cookie en peticiones entre sitios. Un sitio es el dominio registrable (por ejemplo example.com), una idea más grande que la de origen.

ValorLa cookie se envíaContrapartida
StrictSolo en peticiones hechas desde el mismo sitioEl más seguro. Quien llega desde un enlace externo parece "sin sesión" en la primera página
LaxTambién en navegaciones de nivel superior con método seguro, como hacer clic en un enlace (GET)Un buen valor por defecto, pero supone que GET no tiene efectos secundarios
NoneSiempre, incluso entre sitios; exige SecureNinguna protección contra CSRF

Lax no es magia. Si tu sitio tiene GET /account/delete, un enlace en otro sitio que apunte allí será pulsado por la víctima y el navegador envía la cookie, así que la cuenta se borra. Nunca cambies datos con GET. Usa POST, PUT, PATCH o DELETE, protegidos por un token.

Recuerda también que SameSite protege contra otros sitios, no contra otros orígenes del mismo sitio. Si app.example.com y pages.example.com comparten el dominio registrable example.com, y los clientes publican su propio HTML en pages, una página publicada allí es del mismo sitio que tu aplicación. SameSite=Strict no retiene la cookie. Pon el contenido no confiable en un dominio registrable aparte y mantén el token como defensa principal.

Defensa 2: el token anti-CSRF

En el patrón del token sincronizador, el servidor genera un valor aleatorio ligado a la sesión, lo pone en un campo oculto de sus formularios y lo comprueba en cada POST. La cookie va sola, pero el token no se envía automáticamente. La página falsificada no puede leer tu página real para copiar el token, porque la lo prohíbe.

<form method="post" action="/transfer">
  <input type="hidden" name="csrf" value="9f3a...aleatorio-por-sesion">
  <input name="amount">
</form>
<!-- servidor: rechaza el POST si csrf falta o no coincide -->
Un formulario con token

Esto también explica por qué el XSS derrota al token: un script inyectado está en el mismo origen, así que tiene permiso para leer el token.

Dos malentendidos comunes

  • "No configuramos CORS, así que los otros sitios están bloqueados." Falso. La política del mismo origen y CORS protegen la lectura de respuestas entre orígenes. No impiden que un POST simple de formulario se envíe y se procese. El CSRF no necesita leer nada. La defensa debe vivir en el servidor, con token y SameSite.
  • "Nuestra API usa un token Bearer, ¿estamos expuestos?" Poco. Si el token se guarda en memoria y se envía explícitamente en la cabecera Authorization, el navegador no lo adjunta a peticiones falsificadas desde otro sitio. El CSRF es un problema de credenciales ambientales: cookies, autenticación Basic, certificados de cliente. El precio es que los scripts de la página alcanzan el token, así que la defensa contra XSS pesa aún más.
↑ Volver al índice

Autenticación: contraseñas, hashing y más de un factor

Dos palabras se confunden fácil. Autenticación responde "¿quién eres?". Autorización responde "¿puedes hacer esto?". En un cine, mostrar la entrada en la puerta es autenticación; poder entrar solo a la sala 3 y no a la sala 5 es autorización. Un sistema puede hacer la primera perfectamente y la segunda mal, y al revés. Son controles distintos, con fallas distintas.

Cómo guardar una contraseña

El servidor nunca necesita saber tu contraseña, solo reconocerla. Así que guarda la salida de una función de una sola vía y, al iniciar sesión, repite el cálculo con lo que escribiste y compara. Las contraseñas no se cifran (quien obtuviera la clave se llevaría todas las contraseñas); se les aplica hash.

Dos ingredientes hacen que un hash sea apto para contraseñas. Primero, una sal (salt): un valor aleatorio, distinto para cada contraseña y guardado junto al hash. Sin ella, dos usuarios con la misma contraseña tienen el mismo hash, y un atacante puede usar una tabla precalculada de contraseñas comunes para romper muchas cuentas a la vez. Con sal, cada fila es un problema aparte. La sal no es secreta. Segundo, el hash debe ser lento a propósito.

¿Por qué lento? Los hashes rápidos como MD5 o SHA-256 se hicieron para comprobar archivos deprisa, lo cual es estupendo para archivos y terrible para contraseñas, porque quien tiene una tabla robada puede probar millones de intentos por segundo. Argon2id, scrypt y bcrypt añaden un costo que puedes subir con el tiempo; Argon2id y scrypt además exigen mucha memoria, lo que hace caras las tarjetas gráficas (GPU). Un SHA-256 con sal sigue siendo inadecuado: la sal frena el ataque masivo, pero solo el costo frena cada cuenta individual.

Intentos por segundo en un núcleo de CPU, del benchmark de este repositorio (laboratorio de contraseñas y sesiones)
MD5, sin sal
843,636 hashes por segundo
SHA-256 + sal de 16 bytes
548,724 hashes por segundo
Argon2id 19 MiB, t=2
22 hashes por segundo
Argon2id 64 MiB, t=3
3.5 hashes por segundo

Lee el gráfico como el número de intentos por segundo que un atacante puede probar en una tabla robada, por núcleo. Argon2id hace que cada intento cueste unos 45 milisegundos y decenas de megabytes de memoria, aproximadamente 38.000 veces más que MD5 en esa ejecución. Un usuario paga ese costo una vez por inicio de sesión y no lo nota. La máquina, las versiones y el comando están registrados en los resultados del laboratorio.

EsquemaUna víaSalLento y exigente en memoria
Texto planononono
MD5sínono
SHA-256 con salsísíno
Argon2idsísísí

Una pimienta (pepper) es distinta de una sal. La pimienta es un secreto compartido por toda la aplicación y guardado fuera de la base de datos (en una bóveda o en el entorno). Ayuda cuando solo se filtra la base de datos, por ejemplo por una inyección SQL o una copia de seguridad robada. Si se toma el servidor entero, la pimienta se filtra también, así que es una capa extra, no un reemplazo.

¿Y si un sistema viejo guarda solo MD5(contraseña) y queremos a todos protegidos con Argon2id ya, sin conocer las contraseñas? Guarda Argon2id(MD5 viejo) para todas las cuentas a la vez y registra qué esquema usa cada fila. En el siguiente inicio de sesión exitoso de cada usuario tienes la contraseña real en la mano, así que guarda Argon2id simple y descarta la envoltura.

Pantallas de inicio de sesión y políticas

  • Di lo mismo en cada fallo. Si la pantalla responde "usuario no encontrado" para correos desconocidos y "contraseña incorrecta" para los conocidos, regalas una lista de quién tiene cuenta. Responde "credenciales inválidas" siempre, con el mismo código de estado y un tiempo parecido. Lo mismo aplica a las pantallas de registro y de recuperación de contraseña.
  • Limita los intentos, pero piensa en quién controla el contador. Bloquear una cuenta para siempre tras 3 contraseñas erróneas permite que cualquiera bloquee a cualquiera. Usa bloqueos cortos que crecen, límites por cuenta y por origen, y un segundo factor.
  • Sigue la guía actual (NIST SP 800-63B). Exige una longitud mínima, acepta frases largas y cualquier carácter, permite pegar, rechaza contraseñas ya encontradas en filtraciones y no obligues a cambios periódicos sin motivo. La longitud y la unicidad valen más que reglas raras de complejidad.

Factores

Un factor es una categoría de prueba. Hay tres: algo que sabes (contraseña, PIN), algo que tienes (un teléfono, una llave de seguridad) y algo que eres (huella digital, rostro). La autenticación multifactor de verdad combina categorías distintas, para que robar una contraseña no baste. Una contraseña más un código temporal de una app autenticadora son dos factores. Una contraseña más una pregunta secreta no lo son: ambas son "algo que sabes".

↑ Volver al índice

Sesiones y cookies: seguir con la sesión iniciada

Después de iniciar sesión, no quieres escribir la contraseña en cada página. Por eso el servidor le da a tu navegador un identificador de sesión dentro de una cookie, como la pulsera de un parque de diversiones: muéstrala y pasas. Esto también significa que la pulsera es una contraseña temporal. Quien la presenta es tratado como tú.

Por eso el identificador debe ser largo, aleatorio y sin significado: generado por un generador aleatorio criptográfico con al menos 128 bits de entropía (impredecibilidad), y solo una llave hacia datos que el servidor guarda. Un identificador adivinable es una contraseña adivinable. Esta es una buena cookie:

Set-Cookie: __Host-sid=<32 bytes aleatorios>; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=28800
Una cookie de sesión bien protegida
AtributoQué cierra
HttpOnlyLos scripts de la página leyendo la cookie con document.cookie
SecureLa cookie viajando por HTTP plano, donde podría capturarse. Incluso una sola petición de una imagen la filtraría. Va junto con HSTS
SameSite=LaxLa cookie adjuntándose a peticiones en segundo plano iniciadas por otros sitios
prefijo __Host-Otro subdominio o una página HTTP definiendo o sobrescribiendo tu cookie. El navegador solo acepta el nombre si la cookie tiene Secure, Path=/ y ningún Domain
Max-AgeEl navegador guardando la cookie más tiempo del que quieres (el servidor también aplica el límite)

El prefijo __Host- necesita explicación. Las cookies no siguen la misma idea de origen que el resto de la web: un subdominio puede definir una cookie para el dominio padre y así sobrescribir la de la propia aplicación. El prefijo cierra esa puerta y defiende de la fijación desde subdominios menos confiables.

Fijación de sesión

Aquí el atacante no roba la pulsera; te entrega una que ya conoce y espera a que inicies sesión. Si el servidor conserva el mismo identificador después del inicio de sesión, tu autenticación asciende la sesión que el atacante tiene en la mano.

atacante -> navegador de la víctima : id de sesión X (plantado)
víctima  -> servidor                : POST /login + cookie X
servidor                            : X ahora es "alice"  (mismo id conservado)
atacante -> servidor                : GET /me + cookie X  => datos de alice

Corrección: al iniciar sesión, crea un id NUEVO y destruye el viejo.
Fijación de sesión

La regla: cambia el identificador en cada cambio de privilegio (inicio de sesión, cierre de sesión, elevación de acceso).

Terminar una sesión y sus límites

  • Cerrar sesión es una operación del servidor. Borrar la cookie en el navegador no basta. Si el registro sigue válido en el servidor, quien copió el identificador antes (de un computador compartido, de un registro, de un script) todavía puede usarlo. Borra o invalida el registro.
  • Nunca pongas el identificador en la URL (/orders?sid=...). Las URL se copian, se guardan en el historial del navegador y en registros de servidores y proxies, y se envían a otros sitios en la cabecera Referer. Las cookies se diseñaron para llevarlo sin esas fugas.
  • Usa dos tiempos límite. El idle timeout (por ejemplo 15 minutos sin actividad) reduce la ventana de un dispositivo abandonado. El timeout absoluto (por ejemplo 8 horas en total) limita la vida de un identificador capturado, porque un id robado usado continuamente nunca queda inactivo. Las aplicaciones sensibles usan ambos y renuevan el id cada cierto tiempo.
↑ Volver al índice

JWT, OAuth 2.0 y OpenID Connect: tokens e inicio de sesión delegado

Algunos sistemas no guardan una sesión en el servidor. En cambio te dan una nota firmada, un JSON Web Token (JWT), y confían en lo que está escrito en ella, mientras la firma compruebe. Es como una pulsera de concierto con holograma: el guardia no llama a la taquilla, solo revisa el holograma. Un JWT son tres textos unidos por puntos:

  base64url(header) . base64url(payload) . firma
       |                   |                  |
  cómo se firmó         las claims        prueba de que el emisor
  {"alg":"HS256"}     {"sub":"bob",      escribió las dos
                        "exp":1800000900}  primeras partes
Anatomía de un JWT

Dos hechos le dan forma a todo. Primero, el payload solo está codificado en base64url, no cifrado. Cualquiera que tenga el token lo lee con cualquier herramienta, así que nunca pongas secretos ni datos personales innecesarios en él. La firma garantiza que nada cambió y quién lo emitió, no el secreto. Segundo, el header lo escribe quien envía. Todo lo que el verificador lee de él antes de comprobar la firma es una sugerencia de un extraño.

Las comprobaciones que debe hacer un verificador

ComprobaciónPor qué
El alg del header coincide con el algoritmo que el servidor esperaSi no, quien envía decide si habrá alguna verificación. El algoritmo none significa "sin firma"; un verificador que obedece al token acepta una nota falsificada
La firma es válidaPrueba que lo emitió quien tiene la clave y que no cambió
exp es posterior a ahora (una pequeña tolerancia está bien)La expiración limita el daño de un token filtrado. Un verificador que nunca compara exp acepta tokens viejos para siempre
iss es el emisor en el que confíasEl token viene del lugar correcto
aud incluye este servicioUn token hecho para el servicio de informes no debe funcionar en el servicio de pagos, aunque la firma sea correcta

Una firma válida por sí sola no basta: el token puede ser auténtico pero estar vencido, o ser auténtico pero estar destinado a otro servicio. Las bibliotecas maduras comprueban todo esto por defecto y les pasas una lista explícita de algoritmos permitidos (normalmente uno). Los verificadores escritos a mano suelen olvidar pasos.

Otra trampa es el secreto. Con HS256, la misma clave firma y verifica, así que toda la fuerza es la aleatoriedad de la clave. Si la clave es una palabra humana como secret, un atacante que tenga un token capturado puede probar intentos en su propio computador, sin conexión, sin tocar el servidor y sin ningún bloqueo, hasta que uno reproduzca la firma. Entonces puede firmar cualquier payload. Una clave HS256 necesita al menos 256 bits generados al azar, nunca una palabra memorable.

Además, un token sin estado no se puede retirar: tras un cierre de sesión o un robo funciona hasta exp. El diseño habitual es un token de acceso de vida corta (minutos) más un refresh token guardado en el servidor, rotado y revocable.

OAuth 2.0 y OpenID Connect

Imagina que le das al valet la llave del auto, pero una llave especial que solo abre las puertas y arranca el motor, no la guantera ni el maletero. OAuth 2.0 es esa idea: permite que una app actúe en tu nombre con derechos limitados (los scopes), sin ver nunca tu contraseña. Resuelve la autorización delegada: "¿puede esta app acceder a ese recurso en nombre del usuario?" El resultado es un access token destinado a una API.

OAuth no dice quién es el usuario. OpenID Connect (OIDC) es una capa delgada sobre OAuth que añade autenticación. Entrega un ID token, un JWT firmado con la identidad del usuario, y un endpoint de información del usuario.

AspectoAccess tokenID token
Responde¿Puede esta app llamar a esa API, con qué scopes?¿Quién inició sesión?
Destinado aLa API (servidor de recursos)La propia app cliente (su aud es el id del cliente)
¿Se envía a la API?Sí, en Authorization: BearerNo. Una API que acepta ID tokens deja de comprobar que el permiso se concedió para ella

Las apps como las aplicaciones móviles y las single-page applications no pueden guardar un secreto de cliente. El flujo recomendado hoy para todo cliente es el flujo de código de autorización con PKCE, no el antiguo flujo implícito. Con , la app crea un code_verifier aleatorio, envía su hash (code_challenge) al pedir el código y envía el valor original al cambiar el código por tokens. Quien intercepta el código de autorización pero no inició el flujo no tiene el verifier, así que el código le resulta inútil.

↑ Volver al índice

Control de acceso: IDOR, roles y mass assignment

En una escuela, tu carné de estudiante te deja entrar al edificio (autenticación). Pero no abre la sala de profesores ni los casilleros de otros estudiantes (autorización). El control de acceso roto ocurre cuando un programa olvida comprobar la segunda parte. Fue la categoría número uno del OWASP Top 10 (2021).

La forma más famosa es el IDOR (insecure direct object reference, referencia directa insegura a objeto). Un usuario con sesión iniciada cambia /invoices/1001 por /invoices/1002 en la barra de direcciones y ve la factura de otra persona. El número en la URL, el cuerpo o un campo oculto es solo una solicitud. El servidor debe decidir, en cada petición, si este usuario puede acceder a aquel objeto. Haber iniciado sesión no basta.

// Vulnerable: sabe quién eres, nunca pregunta si es tuyo
const invoice = await db.invoices.findById(params.id);

// Corregido: el dueño forma parte de la propia consulta
const invoice = await db.invoices.findOne({
	id: params.id,
	ownerId: session.user.id,
});
if (!invoice) return status(404);
Handler vulnerable y corregido

Poner el dueño en la consulta (WHERE id = $1 AND owner_id = $2), o pasar por una única función de política central, hace que ninguna ruta pueda olvidar la regla.

Dos direcciones de la misma falla

DirecciónA qué llega el usuarioEjemplo
HorizontalDatos de otro usuario del mismo nivelIDOR: leer la factura de otro cliente
VerticalUna función de un rol superiorUn usuario común escribe /admin/users y la página responde

Ocultar el enlace "Gestionar usuarios" a los usuarios comunes es solo navegación. La ruta debe comprobar el rol por sí misma. Todo lo del cliente (páginas, apps, campos ocultos) está bajo el control del usuario. La interfaz puede ocultar cosas por amabilidad, pero la decisión de seguridad ocurre en el servidor, en cada petición, preferiblemente en un único mecanismo reutilizado.

Buenas reglas

  • Denegar por defecto. Rechaza todo excepto lo que una regla permite explícitamente. Con una lista de permitidos, una regla olvidada bloquea un uso legítimo, y alguien se queja pronto. Con una lista de bloqueo, una regla olvidada expone datos en silencio. Estás eligiendo la manera en que el sistema falla.
  • Los ids impredecibles no son control de acceso. Cambiar números secuenciales por UUID aleatorios es un extra útil contra las adivinanzas, pero un identificador está hecho para circular. Si otra persona obtiene el id por cualquier medio, una comprobación ausente aún la deja entrar.
  • Cuidado con el mass assignment. Si una ruta deja a los usuarios editar su propio perfil y hace update(session.user.id, body), un usuario puede añadir "role": "admin" al cuerpo y ascenderse, aunque se haya comprobado la propiedad. La autorización también aplica a los campos. Valida el cuerpo con un esquema que liste los campos permitidos y rechace el resto.
  • Elige el código de rechazo a propósito. Un servicio de repositorios privados puede responder 404 en lugar de 403 cuando el repositorio existe pero no es tuyo, porque un 403 confirma que existe. Ambos son rechazos válidos; el 404 oculta la existencia del recurso. De cualquier modo, la decisión está en el servidor y la respuesta es consistente, tiempo incluido.
↑ Volver al índice

SSRF, path traversal y subida de archivos

Esta sección trata de tres formas de engañar a un servidor para que use algo que no debería: una dirección web, una ruta de archivo o un nombre de archivo que vino de fuera.

SSRF: el servidor servicial

Muchas funciones hacen que el servidor actúe como un navegador: vistas previas de enlaces, "importar desde una URL", webhooks, descargadores de imágenes. Imagina pedirle a un amable empleado de oficina: "por favor, llama a este número por mí". Si tú eliges el número, puedes pedirle que llame a la línea privada del jefe, a la que solo llegan los de dentro de la oficina. En el server-side request forgery (SSRF) el usuario aporta una URL y el servidor la descarga desde dentro de la red de la empresa, así que puede alcanzar localhost, servicios internos y la dirección de metadatos de la nube, que no tienen contraseña porque "solo las máquinas internas pueden conectarse". Tiene su propia categoría en OWASP (A10, 2021).

La corrección sigue a la causa: la aplicación debe decidir a dónde está dispuesta a conectarse, y hacer la comprobación sobre lo que la conexión realmente usa, la dirección numérica, en cada salto.

  • Una lista de bloqueo de texto no funciona. Rechazar URL que contienen localhost o 127.0.0.1 falla, porque la misma dirección tiene muchas grafías y cualquier nombre DNS puede apuntar hacia dentro. Analiza la URL, acepta solo http y https, resuelve el nombre y rechaza direcciones de loopback, privadas y link-local. Cuando se conoce el conjunto de destinos, una lista de permitidos de hosts es más fuerte que cualquier lista de bloqueo.
  • Las redirecciones son una trampa. Un sitio permitido puede responder 302 con un Location que apunta a un servicio interno, y los clientes HTTP siguen las redirecciones por defecto. Desactiva las redirecciones automáticas, síguelas tú hasta un límite pequeño y vuelve a comprobar cada nuevo destino.
  • Comprobar y usar deben ser la misma resolución. Si resuelves el nombre, lo compruebas, y luego le das el nombre al cliente y este vuelve a resolver, un servidor DNS malicioso puede responder con una dirección pública la primera vez y una interna la segunda (DNS rebinding). Resuelve una vez, valida, conéctate a esa misma dirección y conserva el nombre solo para la cabecera Host y para TLS.
  • Añade reglas de red también. Ejecuta el componente que descarga URL de usuarios en un segmento aislado o detrás de un proxy de salida, y autentica entre servicios internos. Las comprobaciones de la aplicación son una capa.
  • Ocultar la respuesta no es una corrección. La petición igual se hace (SSRF ciego).
URL del usuario
  -> analizar: ¿esquema http/https? ¿host en la lista de permitidos?
  -> resolver el host una vez -> ¿todas las direcciones públicas?
  -> conectar a ESA dirección (cabecera Host = nombre)
  -> ¿redirección? empezar de nuevo desde arriba (máx. 3 saltos)
Validación que sigue la conexión real

Path traversal: salir de la carpeta

Una ruta de descarga une la carpeta uploads con el nombre de archivo de la petición. Un nombre como ../../etc/something usa la secuencia .., que significa "sube una carpeta". El sistema operativo la resuelve como siempre, y el archivo leído queda fuera de la carpeta de subidas. Esto rompe la suposición de que un nombre se queda dentro de una carpeta.

// Vulnerable: join confía en el nombre
const target = path.join(UPLOADS, name);

// Corregido: valida el RESULTADO, no la entrada
const target = path.resolve(UPLOADS, name);
if (!target.startsWith(UPLOADS + path.sep)) return status(400);
Vulnerable y corregido

Valida la ruta absoluta final (resolviendo también los enlaces simbólicos) y comprueba que está dentro de la raíz permitida. Mejor aún, nunca aceptes rutas del cliente: recibe un identificador y búscalo en un índice. Ejecuta el proceso con pocos permisos, porque el alcance de la falla es todo lo que el proceso puede leer.

Subida de archivos: el nombre, el tipo y el contenido son afirmaciones

Cuando un usuario envía un archivo, el nombre, la extensión y el Content-Type declarado son solo texto que eligió el cliente. Una cabecera que dice image/png no prueba nada. Una subida segura combina varias medidas.

MedidaPor qué
Generar un nombre aleatorio en el servidor, guardando el nombre original como metadatoEl nombre del cliente nunca se usa en el sistema de archivos, así que no puede recorrer ni sobrescribir
Lista de permitidos de tipos, y comprobar los primeros bytes (la firma del archivo)El tipo declarado es solo una afirmación
Límite de tamañoEvita llenar el disco o la memoria
Guardar fuera de carpetas ejecutables o servidas directamente; enviar X-Content-Type-Options: nosniffUn archivo lleno de HTML no debe ejecutarse como página o programa
↑ Volver al índice

Secretos, dependencias y la cadena de suministro

Un secreto es cualquier cosa que prueba quién eres ante otro sistema: una clave de API, una contraseña de base de datos, una clave privada. Piensa en las llaves de la escuela. No las pegas en la puerta de entrada, ni haces una copia para cada visitante. Los secretos se tratan igual.

Dónde viven los secretos

Un secreto es configuración, no código. Entra al proceso en tiempo de ejecución, desde variables de entorno o desde un gestor de secretos (una bóveda) que controla quién puede leerlo, registra cada uso y facilita la rotación. El repositorio guarda solo los nombres de las variables esperadas, nunca los valores.

¿Y si una persona desarrolladora hace commit de una clave por error y luego borra la línea en un commit nuevo? La clave sigue en el historial, y las copias del repositorio se esparcen rápido. Así que la primera acción eficaz es revocar la clave y emitir una nueva (rotación). Solo después limpia el historial, comprueba si la clave se usó y evita que vuelva a pasar con escaneo de secretos antes de los commits.

  1. Revoca la clave filtrada y crea una nueva.
  2. Revisa en los registros cualquier uso de la clave vieja.
  3. Limpia el historial y las copias, si es posible.
  4. Añade escaneo de secretos antes de los commits para que no vuelva a pasar.

Dependencias: el código de otros

La mayor parte de tu aplicación es código escrito por otras personas: bibliotecas y las bibliotecas de esas bibliotecas. Esa es la cadena de suministro. Dos ideas la protegen.

  • Lockfile. Archivos como bun.lock o package-lock.json registran la versión exacta y el hash de cada dependencia. Sin uno, un rango como ^1.2.0 instala lo más nuevo de ese día, y máquinas distintas pueden ejecutar código distinto. Haz commit del lockfile y úsalo en modo estricto en las compilaciones.
  • Sigue verificando, aunque no cambies nada. Una aplicación publicada hace un año con todas las dependencias auditadas puede ser vulnerable hoy, porque se divulgan fallas nuevas en versiones que ya instalaste. Sabe qué tienes instalado (dependencias indirectas incluidas), sigue los avisos de seguridad, actualiza a menudo y elimina lo que no uses. Esta es la categoría de OWASP Vulnerable and Outdated Components.
↑ Volver al índice

Juntando todo: una falla, un hábito

Todas estas fallas parecen distintas, pero la mayoría comparte una de dos raíces. O el programa mezcló datos con código (inyección, XSS), o el programa confió en una afirmación de fuera (un id, un rol, un nombre de archivo, una URL, un header de token) sin comprobarla en el servidor. La tabla de abajo conecta cada falla con el hábito que la frena.

Falla, causa y defensa principal
FallaCausa raízDefensa principalCapas extra
Inyección SQL / de comandosDatos pegados en el texto del comandoParámetros, listas de argumentosValidación, mínimo privilegio, lista de permitidos para la estructura
XSSDatos interpretados como marcado o scriptCodificación en la salida, textContentSanitizador, CSP, comprobación del esquema de la URL
CSRFEl navegador envía cookies por sí soloToken anti-CSRFSameSite, sin cambiar estado con GET
Contraseñas débilesHash rápido o sin salArgon2id / bcrypt con salPimienta, MFA, límites de intentos
Robo / fijación de sesiónId predecible o reutilizado, el servidor conserva la sesión tras cerrar sesiónId aleatorio, id nuevo al iniciar sesión, cierre de sesión en el servidorHttpOnly, Secure, __Host-, tiempos límite
Errores de JWTConfiar en el header y saltarse las claimsAlgoritmo fijado, clave fuerteComprobaciones de exp, iss, aud
IDOR / mass assignmentSin comprobación de dueño ni de campoAutorizar en el servidor, en cada peticiónDenegar por defecto, lista de campos permitidos
SSRFEl servidor descarga una dirección elegida por el usuarioLista de permitidos + comprobación de la dirección resueltaRedirecciones manuales, conexión fijada, reglas de salida de red
Path traversal / subidaSe acepta el nombre o tipo del clienteValidar la ruta resuelta, nombre de archivo aleatorioComprobación de la firma del contenido, nosniff
Secreto filtradoSecreto en el repositorioEntorno o bóveda, rotarEscaneo de secretos
Componente desactualizadoFallas conocidas en bibliotecas viejasLockfile + avisos + actualizacionesEliminar paquetes sin usar

Errores comunes que evitar

  • Corregir el síntoma (prohibir algunos caracteres) en lugar de la causa (mantener separados código y datos).
  • Ocultar cosas en la interfaz y llamarlo control de acceso.
  • Usar una defensa y detenerse. Las capas fallan de una en una; apílalas.
  • Tratar el OWASP Top 10 como una lista completa o un certificado.
  • Creer que cifrar o aplicar hash a algo es lo mismo que autenticarlo.
↑ Volver al índice