Panel de la demostración - sitio intencionalmente vulnerable con fines educativos (Unidad 4, Seguridad Informática - UNPAZ)
Centro de control MODO: VULNERABLE

Seguridad en el desarrollo de software

Por qué una app que "anda bien" puede ser insegura

Unidad 4 - Seguridad Informática - Lic. en Gestión de Tecnologías de la Información (UNPAZ)

Trabajo práctico. Demostración en vivo con una app propia: "Notas".

Seguridad en el desarrollo: de qué hablamos

La mayoría del software no es malicioso, pero puede tener vulnerabilidades que, si se explotan, permiten que un atacante tome el control o acceda a datos que no le corresponden. La raíz casi siempre es la misma: confiar en datos que vienen de afuera.

Para desarrollar seguro hay que tener en cuenta, entre otras cosas:

  • Validación de la entrada: nunca confiar en lo que manda el usuario; usar listas blancas y escapar metacaracteres.
  • Autenticación y gestión de sesiones: contraseñas con hash+salt, bloqueo por intentos, cookies e IDs de sesión seguros.
  • Control de acceso: autenticación, autorización y auditoría; denegar por defecto.
  • Criptografía: proteger datos sensibles; no inventar algoritmos ni hardcodear claves (van en variables de entorno).
  • Configuración / hardening: minimizar la superficie de ataque y no filtrar información.
  • Mínimos privilegios y dependencias actualizadas; registro y monitoreo.
OWASP Top 10 - 2021

El estándar de referencia

OWASP es una organización sin fines de lucro; su Top 10 lista los riesgos más críticos en aplicaciones web, ordenados por criticidad. Riesgo = probabilidad x impacto.

A01 Control de Acceso Roto. El usuario accede a datos o acciones fuera de sus permisos (ej.: ver la cuenta de otro cambiando la URL).
A02 Fallas Criptográficas. Datos sensibles sin proteger: cifrado ausente o débil, hashes obsoletos (MD5), claves hardcodeadas.
A03 Inyección (incluye XSS). Datos no confiables llegan a un intérprete (SQL, comandos, HTML/JS) y ejecutan algo no previsto.
A04 Diseño Inseguro. Falla de concepto: faltan controles pensados desde el diseño, no es un bug puntual.
A05 Configuración Incorrecta. Defaults inseguros, cuentas por defecto, listados de directorios y errores expuestos, cabeceras faltantes.
A06 Componentes Vulnerables/Obsoletos. Librerías o frameworks desactualizados o con CVE conocidos, corriendo con los privilegios de la app.
A07 Fallas de Identificación y Autenticación. Login y sesiones mal hechos: fuerza bruta, claves débiles, session IDs predecibles o mal invalidados.
A08 Fallas de Integridad de Datos/Software. Actualizaciones, datos o pipelines sin verificar integridad; deserialización insegura.
A09 Fallas de Registro y Monitoreo. Sin logs ni alertas: los ataques pasan inadvertidos (detección promedio >200 días).
A10 SSRF. El servidor hace peticiones a URLs que controla el atacante, hacia recursos internos o externos.

Resaltados en amarillo (A01, A02, A03, A05 y A07): los que se demuestran con código en esta presentación.

Cómo se demuestra

  • Armé una app real de notas privadas: la promesa es "solo vos ves tus notas".
  • Node + Express + EJS + SQLite. La sesión viaja en una cookie con un identificador (sid).
  • Un interruptor global cambia toda la app entre modo VULNERABLE y modo SEGURO.
  • Para el usuario la app se ve igual; lo que cambia es el código que corre por debajo.

La idea de cada lámina: el mismo ataque funciona en VULNERABLE y falla en SEGURO. Se compara el código lado a lado.

A03:2021 - Inyección (XSS reflejado)

Reflejar la entrada sin escapar

Inseguro

src/views/buscar.ejs
Resultados para: <%- q %>

<%- %> imprime el texto tal cual. Si q es un <script>, se ejecuta.

Seguro

src/views/buscar.ejs + CSP
Resultados para: <%= q %>

<%= %> escapa el HTML. Además la CSP con nonce bloquea scripts inline.

Cómo vulnerarlo
  1. Situación: la víctima está logueada en Notas (tiene su cookie de sesión).
  2. Vos hacés: le pasás un enlace al buscador con un payload: /buscar?q=<script>...</script>
  3. Pasa: al abrirlo, la página imprime el <script> sin escapar y el navegador de la víctima lo ejecuta como si fuera código del sitio.

En SEGURO: la salida se escapa, el <script> se muestra como texto y la CSP no deja correr scripts inline. No pasa nada.

Riesgo: ejecutar JavaScript en el navegador de la víctima para robarle la sesión o redirigirla.

A03:2021 - Inyección (XSS almacenado)

Guardar contenido de usuarios y mostrarlo a otros

Inseguro

src/views/libro.ejs
<%- c.cuerpo %>

El comentario guardado se inyecta sin escapar: ataca a cada visitante.

Seguro

src/views/libro.ejs + CSP
<%= c.cuerpo %>

Se escapa al mostrar. El payload queda como texto, no como código.

Cómo vulnerarlo
  1. Vos hacés: dejás un "comentario" en el libro de visitas cuyo texto es <script>...</script>. Queda guardado en la base.
  2. Pasa: cada persona que entra a la página recibe el script y lo ejecuta. No hace falta mandarle nada a nadie: el ataque queda "plantado".

En SEGURO: al mostrar el comentario se escapa, así que se ve el texto literal <script> en vez de ejecutarse.

Riesgo: un solo comentario malicioso compromete a todos los que abren la página (más grave que el reflejado).

A03:2021 - Inyección (XSS basado en DOM)

El navegador arma HTML con datos de la URL

Inseguro

src/views/saludo.ejs
destino.innerHTML = nombre;

nombre viene de location.hash. innerHTML interpreta HTML.

Seguro

src/views/saludo.ejs
destino.textContent = nombre;

textContent nunca interpreta HTML: inserta texto plano.

Cómo vulnerarlo
  1. Vos hacés: compartís un enlace con el payload después del #: /saludo#<img src=x onerror=alert(1)>
  2. Pasa: el JavaScript de la página toma el texto del # y lo mete con innerHTML, que interpreta el HTML y dispara el código.

En SEGURO: se usa textContent: lo del # se inserta como texto, nunca como HTML.

Riesgo: el payload ni siquiera pasa por el servidor; se ejecuta solo en el navegador.

Cookies de sesión - el corazón del robo de token

Que la cookie sea (o no) legible por JavaScript

Inseguro

src/auth.js
sid=abc123; Path=/; SameSite=None
// sin HttpOnly

document.cookie la lee -> el XSS la roba y la manda al atacante.

Seguro

src/auth.js
sid=abc123; Path=/; SameSite=Lax;
  HttpOnly; Secure

HttpOnly: el JavaScript NO puede leerla. El robo por XSS deja de funcionar.

Riesgo: con la cookie robada, el atacante se hace pasar por la víctima (session hijacking) y lee sus notas privadas.

A07:2021 - Gestión de sesiones (Session Hijacking)

Robar y secuestrar la sesión, paso a paso

El servidor no pide usuario y contraseña en cada clic: identifica a la persona por el sid que viaja en la cookie. Si consigo tu sid, soy vos.

La cadena del ataque (modo VULNERABLE)
  1. La víctima inicia sesión en Notas. Su navegador guarda sid=abc123.
  2. El atacante le manda su enlace trampa (el del buscador con XSS).
  3. La víctima lo abre estando logueada. El script lee document.cookie y manda el sid al atacante (endpoint /c/:token).
  4. El sid aparece en el panel de loot del atacante, en vivo.
  5. El atacante toca "Secuestrar sesión".

Qué hace "Secuestrar sesión"

src/routes/attacker.js
res.append('Set-Cookie',
  `sid=${sidRobado}; Path=/; ...`);
res.redirect('/notas');

Pega el sid de la víctima como su propia cookie y entra a /notas. El server cree que es la víctima: le muestra sus notas privadas. Nunca hizo falta la contraseña.

Por qué en SEGURO falla

// cookie: HttpOnly + Secure
// + CSP con nonce

El paso 3 nunca ocurre: con HttpOnly el script no puede leer document.cookie, y la CSP ni siquiera deja correr el script. El panel de loot queda vacío, no hay sid que secuestrar.

Riesgo: suplantación total de identidad (session hijacking). El atacante opera como la víctima sin conocer su clave.

Caja de herramientas del navegador

Cómo se mira en Chrome y Firefox

No hace falta una app extra: se abre la caja de herramientas del propio navegador y se ve lo que viaja (cookies, cabeceras, CSP).

Abrir DevTools
  1. Chrome: F12, o clic derecho → Inspeccionar. Firefox: F12, o clic derecho → Inspeccionar.
  2. Consola: se escribe document.cookie y Enter. En modo VULNERABLE aparece sid=... (la cookie es legible por JavaScript). En modo SEGURO no aparece: está marcada HttpOnly.
  3. Cookies: en Chrome, pestaña Application → Storage → Cookies → el dominio. En Firefox, pestaña Almacenamiento → Cookies. La columna HttpOnly dice si el script puede leerla.

Pestaña Network / Red

Se recarga y se filtra por login o /c/. Ahí se ve el POST del login, el GET a /c/:token cuando el XSS manda la cookie, la respuesta Set-Cookie y (en SEGURO) la cabecera Content-Security-Policy.

Qué cambia en SEGURO

document.cookie no muestra el sid. En la tabla de cookies, HttpOnly está tildado. El XSS del buscador no llega a /c/:token porque la CSP bloquea el script.

Para la demo: con DevTools abierto se muestra en vivo la diferencia entre "la cookie se puede robar" y "el navegador la esconde".

A03:2021 - Inyección (SQL Injection)

Armar la consulta SQL concatenando texto

Inseguro

src/routes/auth.js
const q = `SELECT * FROM usuarios
  WHERE email = '${email}'
  AND password_md5 = '${md5(pass)}'`;
db.prepare(q).get();

Payload en email: ' OR 1=1 -- (espacio tras --) entra sin contraseña (tautología).

Seguro

src/routes/auth.js
db.prepare('SELECT * FROM usuarios
  WHERE email = ?').get(email);
// + bcrypt.compare + bloqueo 5 intentos

Consulta parametrizada: el dato nunca se interpreta como SQL.

Cómo vulnerarlo
  1. Vos hacés: en el campo email escribís ' OR 1=1 -- y dejás la contraseña vacía.
  2. Pasa: la consulta queda ... WHERE email='' OR 1=1 -- ' AND password_md5=.... El OR 1=1 es siempre verdadero y el -- comenta el chequeo de contraseña, así que entras como el primer usuario.

En SEGURO: el email viaja como parámetro (?), nunca se interpreta como SQL; además valida con bcrypt y bloquea a los 5 intentos.

Riesgo: saltear el login, leer o borrar toda la base de datos.

A03:2021 - Inyección SQL (práctica)

Forzar el login en modo VULNERABLE

Pasos en el navegador
  1. El interruptor de /admin queda en VULNERABLE.
  2. Se abre /login?demo=1.
  3. En email se pega exactamente: ' OR 1=1 -- (hay un espacio después de --). La contraseña se deja vacía.
  4. Se envía el formulario. Entra como el primer usuario, sin conocer la clave.

Por qué entra

src/routes/auth.js
WHERE email = '' OR 1=1 -- ' AND password_md5 = '...'
// OR 1=1 es siempre verdadero
// -- comenta el chequeo de la contraseña

Es una sola sentencia (tautología). .get() de SQLite no corre varias consultas, pero acá no hace falta: con una alcanza.

Qué se ve en Red

En Network/Red, el POST a /login responde 302 y un Set-Cookie: sid=.... En la Consola, document.cookie muestra ese sid si el modo sigue en VULN.

En SEGURO: el email viaja como parámetro, no como SQL. El mismo payload no entra.

Ojo: el espacio después de -- importa: en SQL el comentario -- lleva un espacio. Sin ese espacio, el AND password_md5 no se comenta y el login falla.

A03:2021 - Inyección (comandos del SO)

Pasar la entrada a un comando del sistema

Inseguro

src/routes/modules.js
ejecutar("ping " + host)
// host = "8.8.8.8; cat /etc/passwd"

Los metacaracteres ; | && encadenan comandos extra.

Seguro

src/routes/modules.js
/^(\d{1,3}\.){3}\d{1,3}$/.test(host)
// allowlist: solo IP o hostname valido

Se valida con lista blanca; cualquier metacaracter se rechaza.

Cómo vulnerarlo
  1. Vos hacés: en el campo "host" para hacer ping escribís 8.8.8.8; cat /etc/passwd.
  2. Pasa: el servidor arma ping 8.8.8.8; cat /etc/passwd. El ; encadena un segundo comando y te devuelve el contenido de archivos del sistema.

En SEGURO: una lista blanca (regex) acepta solo una IP/hostname válido; cualquier ; | && se rechaza.

Riesgo: ejecutar comandos arbitrarios en el servidor con los permisos del proceso web.

A02:2021 - Fallas criptográficas (contraseñas)

Cómo se guardan las contraseñas

Inseguro

src/db.js
md5(password)
// misma pass => mismo hash
// sin salt, rapido de crackear

Vulnerable a diccionario y rainbow tables.

Seguro

src/db.js
bcrypt.hashSync(password, 10)
// salt por usuario + lento a proposito

Misma pass => hashes distintos; ataques carísimos.

Cómo vulnerarlo
  1. Situación: conseguiste la base (por SQLi o por el /backup.sql expuesto). Tenés los hashes MD5.
  2. Vos hacés: buscás cada hash en una tabla precalculada (rainbow table) o lo comparás con un diccionario. MD5 es instantáneo.

En SEGURO: bcrypt es lento a propósito y cada clave tiene su propio salt, así que ni rainbow tables ni diccionarios masivos sirven.

Riesgo: si se filtra la base, con MD5 las contraseñas caen en segundos.

A02 / A07:2021 - 2FA por email (OTP)

El código de un solo uso

Inseguro

src/routes/modules.js
Math.random() -> 4 digitos
guardado en texto plano
sin expiracion ni limite

Predecible, reutilizable, fuerza bruta (10.000 opciones).

Seguro

src/routes/modules.js
crypto.randomInt() -> 6 digitos
se guarda SHA-256(codigo)
expira 10min, un solo uso,
timingSafeEqual

Hash, expiración, límite de intentos y comparación en tiempo constante.

Cómo vulnerarlo
  1. Vos hacés: como el código es de 4 dígitos, no expira y no limita intentos, los probás todos (0000..9999) o aprovechás que Math.random() es predecible.
  2. Pasa: adivinás el OTP de la víctima y completás el segundo factor sin tener su teléfono/mail.

En SEGURO: 6 dígitos con crypto.randomInt, se guarda solo el hash, expira a los 10 min, un solo uso y límite de intentos: la fuerza bruta no llega.

Hash vs cifrado: el OTP se hashea (no hace falta recuperarlo). Un secreto TOTP se cifra (AES-256-GCM) porque el servidor necesita leerlo.

A01:2021 - Control de acceso roto (IDOR)

Pedir un recurso por id sin verificar el dueño

Inseguro

src/routes/modules.js
SELECT * FROM notas WHERE id = ?

/notas?id=4 devuelve la nota privada de otra cuenta.

Seguro

src/routes/modules.js
SELECT * FROM notas
  WHERE id = ? AND usuario_id = ?
// si no, 403

Deny by default: solo accedés a lo tuyo.

Cómo vulnerarlo
  1. Situación: entras con TU cuenta y ves tus notas en /notas. Cada una tiene un id (la tuya es, por ejemplo, la #7).
  2. Vos hacés: cambiás el número en la URL a mano: /notas?id=4.
  3. Pasa: la consulta pide la nota 4 sin verificar que sea tuya, así que te devuelve la "Nota privada del Admin". Probás id=1, 2, 3... y leés las de todos.

En SEGURO: la consulta agrega AND usuario_id = <el tuyo>. Pedir el id de otro no devuelve nada: responde 403 (denegar por defecto).

Riesgo: leer o modificar datos de otros usuarios cambiando un número en la URL. No hace falta hackear nada: solo editar la dirección.

A05:2021 - Configuración incorrecta

Cabeceras de seguridad y errores

Inseguro

src/security/headers.js
// sin CSP, sin X-Frame-Options
X-Powered-By: Express
// stack trace visible, /backup.sql expuesto

Expone tecnología, permite clickjacking y filtra info.

Seguro

src/security/headers.js
Content-Security-Policy: ... 'nonce-...'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Strict-Transport-Security: ...

Defensa en profundidad; errores genéricos.

Cómo vulnerarlo
  1. Vos hacés: probás rutas típicas como /backup.sql o forzás un error en la app.
  2. Pasa: /backup.sql te entrega usuarios y hashes, y los errores muestran el stack trace completo (rutas internas, versiones, consultas). Con eso planeás el siguiente ataque.

En SEGURO: /backup.sql da 404, los errores son genéricos y las cabeceras (CSP, X-Frame-Options, nosniff, HSTS) cierran clickjacking y fugas.

Riesgo: dar información y superficie de ataque gratis al atacante.

En resumen

  • Nunca confiar en la entrada del usuario: validar y escapar.
  • Consultas parametrizadas, nunca concatenar SQL.
  • Cookies HttpOnly + Secure + SameSite.
  • CSP y cabeceras de seguridad siempre.
  • Contraseñas con bcrypt; secretos en variables de entorno.
  • Verificar autorización en cada acceso (deny by default).

Probalo vos: cambia el modo en /admin y repetí cualquier ataque.

1 / 18