Los 10 Riesgos Principales
Broken Object Level Authorization (BOLA)
Vector de ataque
Modificación de identificadores en la URL o cuerpo para acceder a objetos ajenos.
Debilidad
Falta de verificación de permisos a nivel de objeto.
Impacto
Acceso no autorizado a datos de otros usuarios.
Estrategias de mitigación
- Validar siempre que el usuario autenticado tenga acceso al recurso.
- Implementar lógica de autorización robusta basada en el usuario.
- Evitar confiar en los identificadores enviados por el cliente.
Ejemplo en Express
// Código vulnerable
// API permite acceder a cualquier recurso por ID sin verificación
app.get('/api/users/:id', async (req, res) => {
const user = await db.findUserById(req.params.id);
res.json(user);
});// Código seguro
app.get('/api/users/:id', async (req, res) => {
const requestedId = req.params.id;
const authenticatedId = req.user.id; // obtenido del token/session
if (requestedId !== authenticatedId) {
return res.status(403).json({ message: 'Unauthorized access' });
}
const user = await db.findUserById(requestedId);
res.json(user);
});// Código vulnerable
app.put('/api/orders/:orderId', async (req, res) => {
const order = await db.findOrderById(req.params.orderId);
order.status = req.body.status;
await db.save(order);
res.send("Order updated");
});// Código seguro
app.put('/api/orders/:orderId', async (req, res) => {
const order = await db.findOrderById(req.params.orderId);
if (order.userId !== req.user.id) {
return res.status(403).send("Unauthorized to modify this order");
}
order.status = req.body.status;
await db.save(order);
res.send("Order updated");
});// Código vulnerable
// Devuelve detalles de todos los reportes relacionados a un proyecto
app.get('/api/projects/:projectId/reports', async (req, res) => {
const reports = await db.getReportsByProject(req.params.projectId);
res.json(reports);
});// Código seguro
app.get('/api/projects/:projectId/reports', async (req, res) => {
const project = await db.getProjectById(req.params.projectId);
if (!project || project.ownerId !== req.user.id) {
return res.status(403).send("Access denied");
}
const reports = await db.getReportsByProject(project.id);
res.json(reports);
});Broken Authentication
Vector de ataque
Reutilización de credenciales, tokens mal gestionados, sesiones inseguras.
Debilidad
Implementación débil del sistema de autenticación.
Impacto
Suplantación de identidad y acceso completo a la cuenta de un usuario.
Estrategias de mitigación
- Utilizar bibliotecas seguras como Passport.js.
- Aplicar expiración de tokens y rotación periódica.
- Usar autenticación multifactor (2FA).
Ejemplo en Express
// Código vulnerable
app.post('/login', async (req, res) => {
const { username } = req.body;
const user = await db.findUserByUsername(username);
if (!user) return res.status(401).send("Invalid user");
// ❌ No hay validación de contraseña
req.session.userId = user.id;
res.send("Logged in");
});// Código seguro
const bcrypt = require('bcrypt');
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await db.findUserByUsername(username);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).send("Invalid credentials");
}
req.session.userId = user.id;
res.send("Logged in");
});// Código vulnerable
// Login
app.post('/login', async (req, res) => {
const user = await db.findUserByUsername(req.body.username);
req.session.userId = user.id;
res.send("Logged in");
});
// Logout
app.post('/logout', (req, res) => {
// ❌ No se destruye la sesión
res.send("Logged out");
});// Código seguro
// Login con sesión segura y duración limitada
app.post('/login', async (req, res) => {
const user = await db.findUserByUsername(req.body.username);
if (!user) return res.status(401).send("Unauthorized");
req.session.regenerate((err) => {
if (err) return res.status(500).send("Error");
req.session.userId = user.id;
req.session.cookie.maxAge = 15 * 60 * 1000; // 15 minutos
res.send("Logged in");
});
});
// Logout
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) return res.status(500).send("Logout failed");
res.clearCookie('connect.sid');
res.send("Logged out");
});
});// Código vulnerable
const jwt = require('jsonwebtoken');
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.decode(token); // ❌ decode sin verificación
res.send(`Welcome ${payload.username}`);
});// Código seguro
const jwt = require('jsonwebtoken');
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
res.send(`Welcome ${payload.username}`);
} catch (err) {
res.status(401).send("Invalid or expired token");
}
});Broken Object Property Level Authorization
Vector de ataque
Manipulación del payload para modificar propiedades no autorizadas.
Debilidad
No se controla qué campos pueden ser escritos por el cliente.
Impacto
Escalada de privilegios o alteración de información crítica.
Estrategias de mitigación
- Validar y filtrar los campos permitidos (whitelisting).
- Descartar cualquier propiedad no esperada en el servidor.
Ejemplo en Express
// Código vulnerable
app.put('/api/users/:id', async (req, res) => {
const updatedUser = await db.updateUser(req.params.id, req.body);
res.json(updatedUser);
});
//El usuario puede enviar un cuerpo como: { "email": "nuevo@mail.com", "role": "admin" }// Código seguro
app.put('/api/users/:id', async (req, res) => {
const updates = { email: req.body.email }; // solo campos permitidos
if (req.body.role) {
// No permitir cambios de rol por usuarios normales
return res.status(403).send("Cannot change role");
}
const updatedUser = await db.updateUser(req.params.id, updates);
res.json(updatedUser);
});// Código vulnerable
app.put('/api/orders/:id', async (req, res) => {
const order = await db.findOrderById(req.params.id);
Object.assign(order, req.body);
await db.save(order);
res.json(order);
});
//Un cliente podría cambiar el estado de su pedido a "shipped" o "paid".// Código seguro
app.put('/api/orders/:id', async (req, res) => {
const order = await db.findOrderById(req.params.id);
// Solo permitir cambiar dirección si es el propietario
if (order.userId !== req.user.id) {
return res.status(403).send("Unauthorized");
}
const allowedUpdates = {
shippingAddress: req.body.shippingAddress
};
Object.assign(order, allowedUpdates);
await db.save(order);
res.json(order);
});// Código vulnerable
// Actualiza configuración del usuario (incluye propiedades anidadas)
app.patch('/api/users/:id/settings', async (req, res) => {
const user = await db.findUserById(req.params.id);
Object.assign(user.settings, req.body);
await db.save(user);
res.send("Settings updated");
});
//El usuario podría enviar: { "2FAEnabled": false, "adminPanelAccess": true }// Código seguro
app.patch('/api/users/:id/settings', async (req, res) => {
const user = await db.findUserById(req.params.id);
if (user.id !== req.user.id) {
return res.status(403).send("Forbidden");
}
// Definir propiedades seguras a modificar
const allowed = ["theme", "language", "notifications"];
for (const key of Object.keys(req.body)) {
if (!allowed.includes(key)) {
return res.status(400).send(`Property '${key}' not allowed`);
}
user.settings[key] = req.body[key];
}
await db.save(user);
res.send("Settings updated");
});Unrestricted Resource Consumption
Vector de ataque
Peticiones masivas o envío de datos muy grandes.
Debilidad
Falta de límites en uso de CPU, RAM, ancho de banda, etc.
Impacto
Denegación de servicio (DoS) o agotamiento de recursos.
Estrategias de mitigación
- Aplicar rate limiting y control de tamaño en payloads.
- Validar tamaños máximos en cargas y consultas.
Ejemplo en Express
// Código vulnerable
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.query.file);
res.download(filePath);
});// Código seguro
const MAX_FILE_SIZE_MB = 50;
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.query.file);
fs.stat(filePath, (err, stats) => {
if (err || stats.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
return res.status(400).send('File too large or not found');
}
res.download(filePath);
});
});// Código vulnerable
app.get('/api/products', async (req, res) => {
const products = await db.getAllProducts(); // puede ser miles
res.json(products);
});// Código seguro
app.get('/api/products', async (req, res) => {
const page = parseInt(req.query.page || '1');
const limit = Math.min(parseInt(req.query.limit || '10'), 100); // máximo 100
const products = await db.getProducts({ page, limit });
res.json(products);
});// Código vulnerable
app.post('/generate-pdf', async (req, res) => {
const { htmlContent } = req.body;
const pdfBuffer = await generatePDF(htmlContent); // sin límite
res.setHeader('Content-Type', 'application/pdf');
res.send(pdfBuffer);
});// Código seguro
app.post('/generate-pdf', async (req, res) => {
const { htmlContent } = req.body;
if (!htmlContent || htmlContent.length > 50000) {
return res.status(400).send("Content too large");
}
try {
const pdfBuffer = await generatePDF(htmlContent);
res.setHeader('Content-Type', 'application/pdf');
res.send(pdfBuffer);
} catch (e) {
res.status(500).send("Error generating PDF");
}
});Broken Function Level Authorization
Vector de ataque
Acceso a funciones restringidas sin verificar permisos de rol.
Debilidad
No se verifica el nivel de privilegio requerido por función.
Impacto
Usuarios no autorizados pueden realizar acciones administrativas.
Estrategias de mitigación
- Verificar explícitamente los roles/autorizaciones por función.
- Seguir el principio de mínimo privilegio.
Ejemplo en Express
// Código vulnerable
app.delete('/admin/users/:id', async (req, res) => {
await db.deleteUser(req.params.id);
res.send("User deleted");
});// Código seguro
app.delete('/admin/users/:id', async (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).send("Forbidden");
}
await db.deleteUser(req.params.id);
res.send("User deleted");
});// Código vulnerable
app.get('/reports/monthly', async (req, res) => {
const report = await db.getMonthlyFinancialReport();
res.json(report);
});// Código seguro
app.get('/reports/monthly', async (req, res) => {
if (!req.user.permissions.includes('VIEW_FINANCIAL_REPORTS')) {
return res.status(403).send("Access denied");
}
const report = await db.getMonthlyFinancialReport();
res.json(report);
});// Código vulnerable
// Endpoint de sistema pensado para tareas internas
app.post('/internal/reset-system', async (req, res) => {
await system.resetAll();
res.send("System reset");
});// Código seguro
app.post('/internal/reset-system', async (req, res) => {
if (!req.user || req.user.role !== 'sysadmin') {
return res.status(403).send("Unauthorized");
}
await system.resetAll();
res.send("System reset");
});Unrestricted Access to Sensitive Business Flows
Vector de ataque
Automatización de flujos como compra rápida, scraping o abusos lógicos.
Debilidad
Falta de validaciones contra abuso de lógica del negocio.
Impacto
Impacto económico, saturación del sistema o fraude.
Estrategias de mitigación
- Monitorear patrones de uso anómalos.
- Limitar acciones repetidas por usuario/IP.
- Aplicar CAPTCHA u otros mecanismos de fricción.
Ejemplo en Express
// Código vulnerable
app.post('/generate-coupon', (req, res) => {
const coupon = generateCoupon();
db.saveCoupon(req.user.id, coupon);
res.json({ coupon });
});// Código seguro
const rateLimit = require("express-rate-limit");
const couponLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutos
max: 1, // 1 intento cada 10 minutos por IP
message: "Too many coupon requests. Try later.",
});
app.post('/generate-coupon', couponLimiter, (req, res) => {
const coupon = generateCoupon();
db.saveCoupon(req.user.id, coupon);
res.json({ coupon });
});// Código vulnerable
app.post('/register', async (req, res) => {
await db.createUser(req.body);
res.send("User created");
});// Código seguro
const captchaValidator = require('./captchaValidator');
app.post('/register', async (req, res) => {
const validCaptcha = await captchaValidator.verify(req.body.captchaToken);
if (!validCaptcha) return res.status(400).send("Invalid CAPTCHA");
await db.createUser(req.body);
res.send("User created");
});// Código vulnerable
app.post('/transfer', async (req, res) => {
await transferFunds(req.user.id, req.body.to, req.body.amount);
res.send("Transfer completed");
});// Código seguro
app.post('/transfer', async (req, res) => {
const recentTransfers = await db.countUserTransfers(req.user.id, { withinMinutes: 10 });
if (recentTransfers >= 3) {
logAnomaly(req.user.id, 'Too many transfers');
return res.status(429).send("Too many transfer attempts");
}
await transferFunds(req.user.id, req.body.to, req.body.amount);
res.send("Transfer completed");
});Server-Side Request Forgery (SSRF)
Vector de ataque
Manipulación del servidor para que realice solicitudes HTTP a destinos arbitrarios.
Debilidad
Falta de validación de URLs proporcionadas por el usuario antes de que el servidor realice solicitudes.
Impacto
Escaneo de puertos internos, acceso a servicios internos, exfiltración de datos o ejecución de código remoto.
Estrategias de mitigación
- Sanitizar y validar todas las entradas de datos del cliente.
- Implementar listas blancas de dominios/IPs permitidos.
- Bloquear tráfico a direcciones IP privadas y localhost.
- Implementar políticas de firewall para bloquear conexiones no esenciales.
- No enviar respuestas en bruto al cliente.
Ejemplo de mitigación de SSRF
// Código vulnerable
// El usuario puede hacer que el servidor haga peticiones arbitrarias
app.post('/fetch', async (req, res) => {
const { url } = req.body;
const response = await fetch(url); // ❌ SSRF posible
const data = await response.text();
res.send(data);
});// Código seguro
const allowedDomains = ['https://api.example.com', 'https://docs.example.com'];
app.post('/fetch', async (req, res) => {
const { url } = req.body;
if (!allowedDomains.some(domain => url.startsWith(domain))) {
return res.status(403).send("URL not allowed");
}
const response = await fetch(url); // ✅ acceso limitado
const data = await response.text();
res.send(data);
});// Código vulnerable
// Permite acceder a URLs internas como http://localhost/admin o 169.254.x.x
app.get('/image-proxy', async (req, res) => {
const { imageUrl } = req.query;
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
res.setHeader('Content-Type', 'image/jpeg');
res.send(Buffer.from(buffer));
});// Código seguro
const dns = require('dns').promises;
const { URL } = require('url');
async function isPrivateIp(urlString) {
try {
const url = new URL(urlString);
const addresses = await dns.lookup(url.hostname, { all: true });
return addresses.some(addr => {
return /^127\./.test(addr.address) || // localhost
/^10\./.test(addr.address) || // red privada
/^192\.168\./.test(addr.address) ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(addr.address);
});
} catch (e) {
return true; // fallback a denegar
}
}
app.get('/image-proxy', async (req, res) => {
const { imageUrl } = req.query;
if (await isPrivateIp(imageUrl)) {
return res.status(403).send("Access to internal resources denied");
}
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
res.setHeader('Content-Type', 'image/jpeg');
res.send(Buffer.from(buffer));
});// Código vulnerable
// Recibe una URL de webhook sin validación
app.post('/register-webhook', async (req, res) => {
const { url } = req.body;
db.saveWebhook(url); // ❌ el servidor luego enviará datos a cualquier URL
res.send("Webhook saved");
});// Código seguro
const validWebhookDomain = /^https:\/\/hooks\.example\.com\/.*/;
app.post('/register-webhook', async (req, res) => {
const { url } = req.body;
if (!validWebhookDomain.test(url)) {
return res.status(400).send("Invalid webhook URL");
}
db.saveWebhook(url); // ✅ solo dominios específicos permitidos
res.send("Webhook saved");
});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, 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).
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'));Improper Inventory Management
Vector de ataque
Acceso a APIs antiguas, no documentadas o desactivadas.
Debilidad
Falta de visibilidad del ciclo de vida de las APIs.
Impacto
Exposición de endpoints obsoletos y vulnerables.
Estrategias de mitigación
- Mantener inventario y control de versiones.
- Aplicar procesos de ciclo de vida (API lifecycle management).
Ejemplo en Express
// Código vulnerable
// ⚠️ Este endpoint fue usado para pruebas internas y quedó accesible en producción
app.get('/test-delete-all-users', async (req, res) => {
await db.deleteAllUsers();
res.send('All users deleted');
});// Código seguro
// ✅ Eliminar endpoints no documentados y que no pertenecen al flujo oficial de la aplicación
// Esta ruta se elimina completamente del código antes del despliegue
// Alternativamente, puede protegerse con autorización y limitarse al entorno de staging// Código vulnerable
// ⚠️ Ruta antigua que sigue funcionando sin autenticación ni control
app.get('/api/v1/user-data', (req, res) => {
res.send(db.getUserById(req.query.id));
});// Código seguro
// ✅ Control de versiones y desactivación explícita de APIs antiguas
app.use('/api/v1/*', (req, res) => {
res.status(410).send("Esta versión de la API está obsoleta");
});
// Solo permitir acceso a /api/v2 con autenticación y control adecuado
app.get('/api/v2/user-data', authenticate, (req, res) => {
res.send(db.getUserById(req.user.id));
});// Código vulnerable
// ⚠️ Ruta de administración visible por convención
app.get('/admin/feature-flags', (req, res) => {
res.json(db.getFeatureFlags());
});// Código seguro
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(helmet()); // 🛡️ Protege cabeceras comunes
// ✅ Middleware de autenticación de administradores
function authenticateAdmin(req, res, next) {
const user = req.user; // Suponiendo que viene desde JWT o sesión
if (!user || !user.roles.includes('admin')) {
return res.status(403).send('Acceso denegado');
}
next();
}
// ✅ Usar rutas menos triviales y fuera de patrones comunes
app.get('/internal/v2/settings/flags', authenticateAdmin, async (req, res) => {
try {
const flags = await db.getFeatureFlags();
res.json({
flags,
accessedAt: new Date().toISOString()
});
} catch (error) {
res.status(500).send('Error al recuperar los flags');
}
});Unsafe Consumption of APIs
Vector de ataque
Consumo de APIs externas sin validar sus datos.
Debilidad
Confianza ciega en terceros sin validación de entrada.
Impacto
Inyección de datos maliciosos o ejecución de código no seguro.
Estrategias de mitigación
- Validar siempre la respuesta de servicios externos.
- Aplicar filtrado y sanitización antes de usar datos de terceros.
Ejemplo en Express
// Código vulnerable
const axios = require('axios');
app.get('/api/currency-exchange', async (req, res) => {
const { from, to } = req.query;
// ⚠️ No validamos la respuesta del servicio externo, lo que puede llevar a datos no deseados o incorrectos
const result = await axios.get(`https://external-api.com/exchange?from=${from}&to=${to}`);
res.send(result.data); // Se asume que la respuesta es siempre correcta
});// Código seguro
const axios = require('axios');
// Función para validar la respuesta de la API externa
function isValidExchangeRateResponse(data) {
return data && typeof data.rate === 'number'; // Validación simple de que existe la propiedad 'rate' y es un número
}
app.get('/api/currency-exchange', async (req, res) => {
const { from, to } = req.query;
try {
const result = await axios.get(`https://external-api.com/exchange?from=${from}&to=${to}`);
// ✅ Validar la respuesta antes de usarla
if (!isValidExchangeRateResponse(result.data)) {
return res.status(500).send('Error en la respuesta de la API externa');
}
res.send(result.data);
} catch (error) {
res.status(500).send('Error al obtener datos de cambio de divisas');
}
});// Código vulnerable
app.get('/api/data', async (req, res) => {
const { userId } = req.query;
// ⚠️ Llamada a API externa sin ningún tipo de autenticación o verificación
const response = await axios.get(`https://external-api.com/data/${userId}`);
res.send(response.data);
});// Código seguro
app.get('/api/data', authenticateUser, async (req, res) => {
const { userId } = req.query;
// ✅ Verificar que el usuario autenticado tenga acceso a los datos solicitados
if (req.user.id !== userId) {
return res.status(403).send('Acceso no autorizado');
}
try {
const response = await axios.get(`https://external-api.com/data/${userId}`, {
headers: { Authorization: `Bearer ${req.user.token}` } // Usar autenticación de API
});
res.send(response.data);
} catch (error) {
res.status(500).send('Error al recuperar los datos');
}
});// Código vulnerable
const axios = require('axios');
app.get('/api/product-info', async (req, res) => {
const { productId } = req.query;
// ⚠️ Llamada a API externa sin validación ni filtrado de la respuesta
const response = await axios.get(`https://external-api.com/product/${productId}`);
// La respuesta es directamente pasada sin ningún control
res.send(response.data);
});// Código seguro
const axios = require('axios');
const { body, validationResult } = require('express-validator'); // Usando express-validator para la validación
// Función para validar la respuesta de un producto
function isValidProductResponse(data) {
return data && data.id && data.name && typeof data.price === 'number'; // Asegurarse de que la respuesta tiene todos los campos necesarios
}
app.get('/api/product-info', [
body('productId').isInt().withMessage('El productId debe ser un número entero'), // Validar productId
], async (req, res) => {
// Validar la entrada del usuario
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { productId } = req.query;
try {
const response = await axios.get(`https://external-api.com/product/${productId}`);
// ✅ Validación de la respuesta
if (!isValidProductResponse(response.data)) {
return res.status(500).send('Datos inválidos recibidos de la API');
}
// ✅ Filtrado de la respuesta para evitar posibles scripts maliciosos
const sanitizedProductData = {
...response.data,
name: xss(response.data.name), // Sanitizar el nombre del producto
description: xss(response.data.description), // Sanitizar la descripción
};
res.send(sanitizedProductData);
} catch (error) {
res.status(500).send('Error al obtener los datos del producto');
}
});