OWASP Top 10 (2025): las vulnerabilidades web más críticas


Publicado originalmente en Bemorex Learn, nuestra plataforma de aprendizaje. · Read in English
Qué es OWASP Top 10?
OWASP (Open Web Application Security Project) pública periodicamente una lista de las 10 vulnerabilidades más críticas en aplicaciones web. Es el estandar de referencia para seguridad web en la industria.
Esta guia cubre cada vulnerabilidad con ejemplos de código vulnerable y su versión segura.
A01: Broken Access Control
El control de acceso roto es la vulnerabilidad número uno. Ocurre cuando los usuarios pueden actuar fuera de sus permisos.
Ejemplos comunes
- Acceder a cuentas de otros usuarios cambiando el ID en la URL
- Elevar privilegios de usuario normal a admin
- Manipular tokens o cookies para saltarse verificaciones
Código vulnerable
// VULNERABLE: no verifica que el usuario sea el dueno del recurso
app.get('/api/users/:id/profile', async (req, res) => {
const profile = await db.getProfile(req.params.id);
res.json(profile); // Cualquier usuario puede ver cualquier perfil
});
Código seguro
// SEGURO: verificar autorización
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
const requestedId = req.params.id;
const authenticatedUserId: string = req.user.id;
if (requestedId !== authenticatedUserId && !req.user.isAdmin) {
return res.status(403).json({ error: 'No autorizado' });
}
const profile = await db.getProfile(requestedId);
res.json(profile);
});
Prevencion
- Denegar acceso por defecto
- Implementar control de acceso en el servidor, nunca solo en el frontend
- Registrar y alertar sobre fallas de control de acceso
- Limitar rate de peticiones a APIs
A02: Cryptographic Failures
Fallas criptograficas incluyen transmitir datos sensibles en texto plano, usar algoritmos obsoletos o gestionar mal las claves.
Errores comunes
// VULNERABLE: hashear con MD5 o SHA1
import crypto from 'crypto';
const hash = crypto.createHash('md5').update(password).digest('hex');
// VULNERABLE: almacenar datos sensibles sin cifrar
localStorage.setItem('creditCard', '4532-1234-5678-9012');
// VULNERABLE: transmitir datos sensibles en la URL
fetch(`/api/reset-password?token=${token}&newPassword=${password}`);
Solucion correcta
- Usa bcrypt, scrypt o Argon2 para passwords
- Cifra datos sensibles en reposo con AES-256
- Usa HTTPS para todo el trafico
- No transmitas datos sensibles en URLs o query parameters
- Genera claves y tokens con funciones criptograficamente seguras
A03: Injection
Las inyecciones ocurren cuando datos no confiables se envian a un interprete como parte de un comando o consulta.
Tipos de inyección
| Tipo | Vector | Ejemplo |
|------|--------|---------|
| SQL Injection | Consultas SQL | ' OR 1=1 -- |
| NoSQL Injection | Consultas MongoDB | {"$gt": ""} |
| Command Injection | Comandos del SO | ; rm -rf / |
| LDAP Injection | Consultas LDAP | *)(uid=*))(|(uid=* |
| Template Injection | Motores de plantillas | {{constructor.constructor('return this')()}} |
Prevencion universal
- Usar consultas parametrizadas siempre
- Validar y sanitizar todo input
- Usar ORMs que parametricen automaticamente
- Aplicar el principio de mínimo privilegio en la base de datos
A04: Insecure Design
El diseño inseguro se refiere a fallas fundamentales en la arquitectura de la aplicación que no se pueden solucionar con mejor implementación.
Ejemplo
Flujo de recuperacion de password INSEGURO:
1. Usuario pide reset
2. Se envia pregunta de seguridad (fácil de adivinar)
3. Se muestra la password en la pantalla
Flujo SEGURO:
1. Usuario pide reset con su email
2. Se genera un token criptografico con expiracion (30 min)
3. Se envia link único al email registrado
4. El token es de un solo uso y se invalida al usarse
Principios de diseño seguro
- Modela las amenazas antes de escribir código
- Aplica defense in depth (multiples capas de seguridad)
- Separa responsabilidades y permisos
- Falla de forma segura (deny by default)
A05: Security Misconfiguration
La configuración incorrecta es una de las vulnerabilidades más frecuentes y fáciles de explotar.
Checklist de configuración
// Verificar configuración de seguridad en Express
interface SecurityConfig {
helmet: boolean;
cors: {
origin: string[];
credentials: boolean;
};
rateLimit: {
windowMs: number;
max: number;
};
production: {
stackTraces: boolean;
debugMode: boolean;
defaultCredentials: boolean;
};
}
const secureConfig: SecurityConfig = {
helmet: true,
cors: {
origin: ['https://tudominio.com'], // NO usar '*'
credentials: true
},
rateLimit: {
windowMs: 15 * 60 * 1000,
max: 100
},
production: {
stackTraces: false, // NUNCA en producción
debugMode: false, // NUNCA en producción
defaultCredentials: false // Cambiar SIEMPRE
}
};
A06: Vulnerable and Outdated Components
Usar dependencias con vulnerabilidades conocidas es como dejar la puerta abierta.
Estrategia de mitigacion
# Auditar dependencias regularmente
npm audit
# Auditar solo producción
npm audit --production
# Corregir automaticamente lo posible
npm audit fix
# Ver dependencias desactualizadas
npm outdated
# Herramientas automaticas
# Activar Dependabot o Renovate en tu repositorio
Politica recomendada
- Auditar dependencias en cada build de CI
- Actualizar dependencias con vulnerabilidades críticas en 24 horas
- Actualizar dependencias con vulnerabilidades altas en 1 semana
- Revisar dependencias transitivas (las que tus dependencias usan)
A07: Identification and Authentication Failures
Las fallas de autenticación permiten a los atacantes comprometer passwords, tokens o explotar fallas de implementación.
Requisitos minimos de password
interface PasswordValidation {
isValid: boolean;
errors: string[];
}
function validatePassword(password: string): PasswordValidation {
const errors: string[] = [];
if (password.length < 12) {
errors.push('Minimo 12 caracteres');
}
if (password.length > 128) {
errors.push('Maximo 128 caracteres');
}
if (!/[A-Z]/.test(password)) {
errors.push('Al menos una mayuscula');
}
if (!/[a-z]/.test(password)) {
errors.push('Al menos una minuscula');
}
if (!/[0-9]/.test(password)) {
errors.push('Al menos un número');
}
if (!/[^A-Za-z0-9]/.test(password)) {
errors.push('Al menos un caracter especial');
}
return { isValid: errors.length === 0, errors };
}
A08: Software and Data Integrity Failures
Ocurre cuando el código o la infraestructura no protegen contra violaciones de integridad: pipelines CI/CD inseguros, actualizaciones sin verificación, dependencias de CDN sin integrity checks.
Subresource Integrity (SRI)
<!-- INSEGURO: cargar script de CDN sin verificación -->
<script src="https://cdn.example.com/lib.js"></script>
<!-- SEGURO: con SRI, el navegador verifica el hash -->
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxAh6VgnSY"
crossorigin="anonymous">
</script>
A09: Security Logging and Monitoring Failures
Sin logs adecuados, no puedes detectar ni responder a ataques.
Qué registrar
interface SecurityLog {
timestamp: string;
event: string;
severity: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
userId: string | null;
ip: string;
details: Record<string, unknown>;
}
function logSecurityEvent(log: SecurityLog): void {
// Enviar a sistema de logging centralizado
console.log(JSON.stringify({
...log,
timestamp: new Date().toISOString()
}));
}
// Eventos críticos que DEBES registrar:
// - Intentos de login fallidos
// - Cambios de password/email
// - Fallas de control de acceso (403)
// - Errores de validación de input sospechosos
// - Cambios de permisos/roles
A10: Server-Side Request Forgery (SSRF)
SSRF ocurre cuando un atacante puede hacer que tu servidor envie peticiones a destinos arbitrarios.
Prevencion
// VULNERABLE: el usuario controla la URL
app.get('/api/fetch-url', async (req, res) => {
const url = req.query.url as string;
const response = await fetch(url); // Puede acceder a red interna!
res.json(await response.json());
});
// SEGURO: validar y restringir URLs
function isUrlAllowed(url: string): boolean {
try {
const parsed = new URL(url);
// Solo HTTPS
if (parsed.protocol !== 'https:') return false;
// Bloquear IPs internas
const blockedPatterns = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^0\./,
/^169\.254\./ // Link-local
];
if (blockedPatterns.some(p => p.test(parsed.hostname))) return false;
// Lista blanca de dominios permitidos
const allowedDomains = ['api.github.com', 'api.example.com'];
return allowedDomains.includes(parsed.hostname);
} catch {
return false;
}
}
Conclusion
El OWASP Top 10 no es solo una lista: es un marco de referencia para construir aplicaciones seguras. Cada vulnerabilidad tiene contramedidas claras y probadas.
La clave esta en integrar la seguridad desde el diseño, no como un parche posterior. Usa esta guia como checklist en cada proyecto y revisa periodicamente las actualizaciones de OWASP.
Ejemplos de código
TypeScript
// A03: Inyección - Ejemplo VULNERABLE vs SEGURO
// VULNERABLE: SQL Injection
async function getUserUnsafe(userId: string): Promise<unknown> {
// NUNCA hagas esto: concatenar input del usuario en SQL
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// Un atacante puede enviar: ' OR '1'='1' --
return db.execute(query);
}
// SEGURO: Consultas parametrizadas
interface User {
id: string;
email: string;
name: string;
}
async function getUserSafe(userId: string): Promise<User | null> {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.execute(query, [userId]);
return result.rows[0] ?? null;
}
// SEGURO: Usar un ORM con tipos
async function getUserWithORM(userId: string): Promise<User | null> {
return await prisma.user.findUnique({
where: { id: userId }
});
}TypeScript
// A07: Fallas de autenticación - Implementación segura
import crypto from 'crypto';
interface HashedPassword {
hash: string;
salt: string;
}
// Hashear passwords correctamente
function hashPassword(password: string): HashedPassword {
const salt = crypto.randomBytes(32).toString('hex');
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
return { hash, salt };
}
function verifyPassword(
password: string,
storedHash: string,
salt: string
): boolean {
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
// Comparacion de tiempo constante para prevenir timing attacks
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(storedHash)
);
}
// Rate limiting para login
const loginAttempts = new Map<string, { count: number; lastAttempt: number }>();
function isRateLimited(ip: string): boolean {
const MAX_ATTEMPTS = 5;
const WINDOW_MS = 15 * 60 * 1000; // 15 minutos
const record = loginAttempts.get(ip);
if (!record) return false;
const elapsed = Date.now() - record.lastAttempt;
if (elapsed > WINDOW_MS) {
loginAttempts.delete(ip);
return false;
}
return record.count >= MAX_ATTEMPTS;
}