🔓
VulnerabilidadesSeguridad WebCasos RealesPentesting

5 Vulnerabilidades que Encontramos en el 90% de las Aplicaciones Modernas

Casos reales de vulnerabilidades que vemos constantemente en startups y cómo evitarlas desde el día uno

📅25 de noviembre de 2025
👤Pentacode
⏱️12 min de lectura

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:

HTTP
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:

HTTP
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:

JavaScript
// ❌ 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:

JavaScript
// ✅ 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):

JavaScript
// 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:

JavaScript
// ❌ 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:

JavaScript
// 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:

HTTP
GET /api/users/me

Respuesta vulnerable:

JSON
{
  "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):

JavaScript
// 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:

JavaScript
// 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:

JavaScript
// 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:

JavaScript
// ❌ 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:

HTTP
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:

JavaScript
// ✅ 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:

JavaScript
// 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:

JavaScript
// 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:

Python
# 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:

JavaScript
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:

JavaScript
// 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):

JavaScript
// 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:

JavaScript
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:

JavaScript
// 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:

JavaScript
// 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:

Compartir artículo:

🛡️

¿Listo para proteger tu aplicación?

Agenda una consultoría gratuita y descubre cómo nuestros servicios de pentesting pueden ayudarte a identificar vulnerabilidades.

Chatea con nosotros