5 Vulnerabilidades que Encontramos en el 90% de las Aplicaciones Modernas
Después de realizar cientos de pentests a startups y aplicaciones modernas, hemos identificado patrones claros. Las mismas vulnerabilidades aparecen una y otra vez, incluso en equipos experimentados.
La buena noticia: todas son prevenibles si sabes qué buscar.
1. Insecure Direct Object Reference (IDOR) - El Rey de las Vulnerabilidades
¿Qué es?
IDOR ocurre cuando puedes acceder a recursos de otros usuarios simplemente cambiando un ID en la URL o request.
Caso Real: Startup de Gestión de Proyectos
Escenario: Una startup de gestión de proyectos con 5,000 usuarios y $2M en funding.
Vulnerabilidad encontrada:
GET /api/projects/12345/documents
Authorization: Bearer user_token_alice
Alice podía acceder a sus documentos en el proyecto 12345. Pero al cambiar el ID:
GET /api/projects/54321/documents
Authorization: Bearer user_token_alice
Alice podía ver todos los documentos del proyecto 54321, incluso si no era miembro.
Código vulnerable:
// ❌ VULNERABLE
app.get('/api/projects/:projectId/documents', authenticateUser, async (req, res) => {
const documents = await db.documents.find({
projectId: req.params.projectId
})
return res.json(documents)
})
Problema: El endpoint verifica que el usuario está autenticado, pero NO verifica que tenga permiso para acceder a ese proyecto específico.
Código seguro:
// ✅ SEGURO
app.get('/api/projects/:projectId/documents', authenticateUser, async (req, res) => {
// Primero: Verificar que el usuario pertenece al proyecto
const membership = await db.projectMembers.findOne({
projectId: req.params.projectId,
userId: req.user.id
})
if (!membership) {
return res.status(403).json({ error: 'No tienes acceso a este proyecto' })
}
// Segundo: Solo entonces obtener los documentos
const documents = await db.documents.find({
projectId: req.params.projectId
})
return res.json(documents)
})
Impacto Real
- 90% de las aplicaciones que probamos tienen al menos un IDOR
- Datos comprometidos: Documentos privados, información financiera, conversaciones
- Severidad: Alta a Crítica
Cómo Protegerte
1. Implementa ACL (Access Control Lists):
// middleware/checkProjectAccess.js
export async function checkProjectAccess(req, res, next) {
const { projectId } = req.params
const userId = req.user.id
const hasAccess = await db.projectMembers.exists({
projectId,
userId,
status: 'active'
})
if (!hasAccess) {
return res.status(403).json({ error: 'Acceso denegado' })
}
next()
}
// Usa el middleware en rutas
app.get('/api/projects/:projectId/documents',
authenticateUser,
checkProjectAccess, // ← Verificación de acceso
async (req, res) => {
// Código seguro aquí
}
)
2. Usa UUIDs en lugar de IDs secuenciales:
// ❌ Predecible
GET /api/users/1
GET /api/users/2
GET /api/users/3
// ✅ No predecible
GET /api/users/f47ac10b-58cc-4372-a567-0e02b2c3d479
3. Implementa scope en queries:
// Siempre filtra por el usuario autenticado
const documents = await db.documents.find({
projectId: req.params.projectId,
project: {
members: {
$in: [req.user.id] // ← Usuario debe ser miembro
}
}
})
2. Exposición de Información Sensible en APIs
¿Qué es?
Las APIs devuelven más información de la necesaria, exponiendo datos sensibles.
Caso Real: Plataforma de E-learning
Vulnerabilidad encontrada:
GET /api/users/me
Respuesta vulnerable:
{
"id": "user_123",
"email": "alice@example.com",
"firstName": "Alice",
"lastName": "Smith",
"password": "$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhwy",
"passwordResetToken": "abc123xyz",
"paymentMethod": {
"cardNumber": "4111111111111111",
"cvv": "123",
"expiryDate": "12/25"
},
"internalNotes": "VIP customer, give 20% discount",
"isTestAccount": false,
"lastLoginIp": "192.168.1.100"
}
Problemas:
- ❌ Password hash expuesto
- ❌ Token de reset expuesto
- ❌ Información de pago completa
- ❌ Notas internas visibles
- ❌ IP del usuario expuesta
Cómo Protegerte
1. Usa DTOs (Data Transfer Objects):
// models/UserDTO.js
export class UserPublicDTO {
constructor(user) {
this.id = user.id
this.email = user.email
this.firstName = user.firstName
this.lastName = user.lastName
this.avatar = user.avatar
this.createdAt = user.createdAt
// Solo campos seguros y necesarios
}
}
// En tu endpoint
app.get('/api/users/me', authenticateUser, async (req, res) => {
const user = await db.users.findById(req.user.id)
const safeUser = new UserPublicDTO(user)
return res.json(safeUser)
})
2. Serializa según el contexto:
// Diferentes serializadores para diferentes contextos
export const userSerializers = {
// Para el propio usuario
self: (user) => ({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
subscription: user.subscription,
preferences: user.preferences
}),
// Para otros usuarios (perfil público)
public: (user) => ({
id: user.id,
firstName: user.firstName,
avatar: user.avatar,
bio: user.bio
}),
// Para admins
admin: (user) => ({
...user,
lastLoginIp: user.lastLoginIp,
registrationIp: user.registrationIp,
internalNotes: user.internalNotes
})
}
3. Usa librerías de serialización:
// Con class-transformer (TypeScript)
import { Exclude, Expose } from 'class-transformer'
export class User {
@Expose()
id: string
@Expose()
email: string
@Exclude() // ← Nunca serializar
password: string
@Exclude()
passwordResetToken: string
@Expose()
firstName: string
@Expose()
lastName: string
}
3. Mass Assignment - El Peligro Invisible
¿Qué es?
Permitir que usuarios modifiquen campos que no deberían poder modificar.
Caso Real: SaaS de Suscripciones
Vulnerabilidad:
// ❌ VULNERABLE
app.patch('/api/users/profile', authenticateUser, async (req, res) => {
await db.users.update(req.user.id, req.body)
return res.json({ success: true })
})
Ataque:
PATCH /api/users/profile
Content-Type: application/json
{
"firstName": "Alice",
"role": "admin", ← ¡Usuario se convierte en admin!
"subscription": "premium", ← ¡Upgrade gratuito!
"credits": 999999 ← ¡Créditos infinitos!
}
Cómo Protegerte
1. Whitelist explícita:
// ✅ SEGURO
app.patch('/api/users/profile', authenticateUser, async (req, res) => {
// Definir explícitamente qué campos se pueden modificar
const allowedFields = ['firstName', 'lastName', 'bio', 'avatar']
const updates = {}
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field]
}
}
await db.users.update(req.user.id, updates)
return res.json({ success: true })
})
2. Usar librerías de validación:
// Con Zod
import { z } from 'zod'
const profileUpdateSchema = z.object({
firstName: z.string().min(1).max(50),
lastName: z.string().min(1).max(50),
bio: z.string().max(500).optional(),
avatar: z.string().url().optional()
})
app.patch('/api/users/profile', authenticateUser, async (req, res) => {
// Validar y filtrar
const validatedData = profileUpdateSchema.parse(req.body)
await db.users.update(req.user.id, validatedData)
return res.json({ success: true })
})
3. Separar campos por roles:
// Solo admins pueden modificar estos campos
const adminOnlyFields = ['role', 'credits', 'subscription', 'isBanned']
app.patch('/api/users/:userId', authenticateUser, async (req, res) => {
const updates = {}
// Campos que todos pueden modificar
const publicFields = ['firstName', 'lastName', 'bio']
for (const field of publicFields) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field]
}
}
// Campos solo para admins
if (req.user.role === 'admin') {
for (const field of adminOnlyFields) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field]
}
}
}
await db.users.update(req.params.userId, updates)
return res.json({ success: true })
})
4. Rate Limiting Ausente - La Puerta Abierta
¿Qué es?
Sin límites de tasa, un atacante puede hacer miles de peticiones por segundo.
Caso Real: App de Reservas
Vulnerabilidad: Endpoint de login sin rate limiting.
Ataque:
# Script del atacante
import requests
emails = [
"admin@company.com",
"support@company.com",
"ceo@company.com"
]
passwords = [
"123456", "password", "admin123",
"welcome", "letmein", "qwerty"
# ... 10,000 contraseñas más
]
for email in emails:
for password in passwords:
requests.post('https://api.com/login', json={
'email': email,
'password': password
})
# Sin rate limiting = 10,000 intentos por segundo
Cómo Protegerte
1. Rate limiting por IP:
import rateLimit from 'express-rate-limit'
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // 5 intentos por ventana
message: 'Demasiados intentos de login. Intenta de nuevo en 15 minutos.',
standardHeaders: true,
legacyHeaders: false
})
app.post('/api/auth/login', loginLimiter, async (req, res) => {
// Lógica de login
})
2. Rate limiting por usuario:
// Con Redis
import Redis from 'ioredis'
const redis = new Redis()
async function checkUserRateLimit(userId, action) {
const key = `ratelimit:${userId}:${action}`
const count = await redis.incr(key)
if (count === 1) {
await redis.expire(key, 60) // 60 segundos
}
if (count > 10) {
throw new Error('Rate limit excedido')
}
}
app.post('/api/posts', authenticateUser, async (req, res) => {
await checkUserRateLimit(req.user.id, 'create_post')
// Crear post
})
3. Rate limiting progresivo (backoff):
// Aumentar el tiempo de espera con cada intento fallido
const failedAttempts = await redis.get(`failed:${email}`)
if (failedAttempts > 3) {
const waitTime = Math.pow(2, failedAttempts) * 1000 // Exponencial backoff
return res.status(429).json({
error: `Espera ${waitTime / 1000} segundos antes de intentar de nuevo`
})
}
5. Logging Insuficiente - Ciego en la Oscuridad
¿Qué es?
No registrar eventos de seguridad críticos, haciendo imposible detectar o investigar ataques.
Caso Real: Startup de Fintech
Una startup de fintech fue hackeada. Los atacantes:
- Robaron $50,000 en transferencias fraudulentas
- Accedieron a datos de 10,000 usuarios
- Permanecieron sin detectar por 3 meses
Problema: No tenían logs. No sabían:
- ¿Cuándo empezó el ataque?
- ¿Qué cuentas fueron comprometidas?
- ¿Cómo entraron?
- ¿Qué datos fueron accedidos?
Cómo Protegerte
1. Registra eventos críticos:
import winston from 'winston'
const securityLogger = winston.createLogger({
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'security.log' })
]
})
// Eventos a registrar SIEMPRE
const criticalEvents = {
// Autenticación
LOGIN_SUCCESS: 'login_success',
LOGIN_FAILED: 'login_failed',
LOGOUT: 'logout',
PASSWORD_RESET: 'password_reset',
PASSWORD_CHANGED: 'password_changed',
// Autorización
ACCESS_DENIED: 'access_denied',
PRIVILEGE_ESCALATION_ATTEMPT: 'privilege_escalation',
// Datos sensibles
SENSITIVE_DATA_ACCESS: 'sensitive_data_access',
SENSITIVE_DATA_EXPORT: 'sensitive_data_export',
SENSITIVE_DATA_DELETION: 'sensitive_data_deletion',
// Financiero
PAYMENT_INITIATED: 'payment_initiated',
PAYMENT_COMPLETED: 'payment_completed',
PAYMENT_FAILED: 'payment_failed',
// Admin
USER_ROLE_CHANGED: 'user_role_changed',
SETTINGS_CHANGED: 'settings_changed'
}
// Función helper para logging
function logSecurityEvent(event, data) {
securityLogger.info({
event,
timestamp: new Date().toISOString(),
...data
})
}
// Uso en endpoints
app.post('/api/auth/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password)
if (user) {
logSecurityEvent(criticalEvents.LOGIN_SUCCESS, {
userId: user.id,
email: user.email,
ip: req.ip,
userAgent: req.get('user-agent')
})
return res.json({ token: generateToken(user) })
} else {
logSecurityEvent(criticalEvents.LOGIN_FAILED, {
email: req.body.email,
ip: req.ip,
userAgent: req.get('user-agent')
})
return res.status(401).json({ error: 'Credenciales inválidas' })
}
})
2. Alertas automáticas:
// Detectar patrones sospechosos
async function detectSuspiciousActivity(userId) {
const recentFailedLogins = await countFailedLogins(userId, '1h')
if (recentFailedLogins > 10) {
await sendAlert({
type: 'SUSPICIOUS_ACTIVITY',
message: `Usuario ${userId} tiene ${recentFailedLogins} intentos fallidos en la última hora`,
severity: 'HIGH'
})
}
}
3. Retención de logs:
// Configuración de retención
const logRetention = {
security: '1 year', // Logs de seguridad: 1 año
access: '90 days', // Logs de acceso: 90 días
error: '30 days', // Logs de error: 30 días
debug: '7 days' // Logs de debug: 7 días
}
Checklist de Seguridad Rápida
Antes de lanzar tu app, verifica:
Autorización
- Todos los endpoints verifican permisos de acceso
- IDs no predecibles (UUIDs)
- Tests de IDOR implementados
Exposición de Datos
- DTOs/serializadores implementados
- Campos sensibles excluidos de responses
- Diferentes niveles de acceso según rol
Validación de Inputs
- Whitelist de campos modificables
- Validación de tipos con schemas
- Sanitización de inputs
Rate Limiting
- Límites en endpoints de autenticación
- Límites en operaciones costosas
- Backoff progresivo implementado
Logging
- Eventos de seguridad registrados
- Logs con contexto completo (IP, user agent, etc.)
- Alertas para actividad sospechosa
Conclusión
Estas 5 vulnerabilidades representan el 90% de los problemas que encontramos en pentests. La buena noticia: todas son prevenibles con las prácticas correctas desde el día uno.
No esperes a tener un incidente de seguridad. Implementa estas protecciones hoy.
¿Quieres saber si tu aplicación tiene estas vulnerabilidades? En Pentacode realizamos pentests especializados para startups con reportes claros y accionables. Agenda una consultoría gratuita.
Artículos relacionados: