NexusTech Logo
SeguridadWebOWASPDevSecOps / 31/5/2024

Seguridad Web 2024: Protege tu Aplicación de Ataques Reales

Seguridad Web 2024: La Guía Práctica contra Ataques Reales

El costo promedio de una violación de datos es $4.45 millones. El 83% de las brechas de seguridad se deben a vulnerabilidades web conocidas y prevenibles.

Si aún esperas que “la seguridad es responsabilidad del equipo de DevOps”, estás cometiendo el error que cometen el 60% de los desarrolladores.

OWASP Top 10: Las Vulnerabilidades Más Críticas

1️⃣ Inyección SQL (SQL Injection)

El Problema:

// ❌ VULNERABLE - Nunca hagas esto
const userId = req.query.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query); // ¡Desastre!

Un atacante puede enviar:

?id=1; DROP TABLE users; --

Resultado: Tu tabla de usuarios desaparece.

La Solución: Prepared Statements

// ✓ SEGURO - Usar prepared statements
const userId = req.query.id;
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]); // El motor separa datos de código

// Con TypeORM
const user = await User.findOne({ where: { id: userId } });

// Con Prisma
const user = await prisma.user.findUnique({
  where: { id: parseInt(userId) },
});

Impacto Evitado:

  • Pérdida de datos completa
  • Exposición de información sensible
  • Manipulación de registros

2️⃣ Cross-Site Scripting (XSS)

El Problema:

<!-- ❌ VULNERABLE -->
<h1><?php echo $_GET['name']; ?></h1>

<!-- Si alguien visita: ?name=<script>alert('Hacked!')</script> -->
<!-- El script se ejecuta en el navegador de todos los usuarios -->

Tipos de XSS:

Reflected XSS

URL maliciosa: https://example.com/?search=<img src=x onerror="alert('XSS')">

Stored XSS

// ❌ Comentario guardado sin sanitizar
const comment = req.body.text; // "<script>steal_cookies()</script>"
db.comments.insert(comment);

// Cuando otros ven el comentario, el script se ejecuta

La Solución: Sanitización y Context-Aware Encoding

// ✓ Opción 1: Librerías de sanitización
const DOMPurify = require('isomorphic-dompurify');
const clean = DOMPurify.sanitize(userInput);

// ✓ Opción 2: Template engines seguros
// En React (escape automático)
<div>{userInput}</div> // React escapa automáticamente

// En Vue
<div>{{ userInput }}</div> // Vue escapa por defecto

// ✓ Opción 3: Content Security Policy
// En servidor
res.setHeader('Content-Security-Policy', "script-src 'self'");

Impacto Evitado:

  • Robo de cookies/sesiones
  • Keylogging en navegador
  • Phishing dentro de tu aplicación
  • Difusión de malware

3️⃣ Autenticación Rota

El Problema:

// ❌ Problemas comunes
1. Contraseñas en texto plano
2. Sesiones sin expiración
3. Recuperación de contraseña débil
4. Falta de rate limiting

// ❌ Código inseguro
const login = (email, password) => {
  // Sin encriptación
  const user = db.users.findOne({ email, password });
  // Sin verificación de intentos
  return user;
};

La Solución: Autenticación Segura

// ✓ Encriptación de contraseñas
const bcrypt = require('bcrypt');

const register = async (email, password) => {
  const hashed = await bcrypt.hash(password, 10);
  return db.users.create({ email, passwordHash: hashed });
};

const login = async (email, password) => {
  const user = db.users.findOne({ email });
  if (!user) return null;
  
  const isValid = await bcrypt.compare(password, user.passwordHash);
  return isValid ? user : null;
};

// ✓ Rate limiting
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // 5 intentos
  message: 'Demasiados intentos de login',
});

app.post('/login', loginLimiter, login);

// ✓ Sesiones seguras
app.use(session({
  secret: process.env.SESSION_SECRET,
  httpOnly: true, // No accesible desde JavaScript
  secure: true, // Solo HTTPS
  maxAge: 15 * 60 * 1000, // 15 minutos
  sameSite: 'strict', // CSRF protection
}));

// ✓ Multi-factor authentication
const speakeasy = require('speakeasy');

const enable2FA = (user) => {
  const secret = speakeasy.generateSecret({
    name: `App (${user.email})`,
  });
  return secret; // Usuario escanea el QR
};

const verify2FA = (user, token) => {
  return speakeasy.totp.verify({
    secret: user.twoFactorSecret,
    encoding: 'base32',
    token,
  });
};

Impacto Evitado:

  • Acceso no autorizado
  • Robo de identidad
  • Compromiso de cuentas administrativas

4️⃣ Broken Access Control

El Problema:

// ❌ VULNERABLE - Confía en la UI
// Frontend oculta botón de admin, pero...
app.get('/api/users/:id', (req, res) => {
  // Sin verificar permisos
  res.json(db.users.findById(req.params.id));
});

// Un atacante puede:
// GET /api/users/1 → acceso normal
// GET /api/users/2 → acceso a otro usuario
// GET /api/users/admin → acceso a admin

La Solución: Control de Acceso en Backend

// ✓ Middleware de autorización
const authorize = (roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Not authorized' });
    }
    
    next();
  };
};

// ✓ Uso
app.get('/api/users/:id', authorize(['admin']), (req, res) => {
  res.json(db.users.findById(req.params.id));
});

app.delete('/api/users/:id', authorize(['admin']), (req, res) => {
  db.users.deleteById(req.params.id);
  res.json({ success: true });
});

// ✓ Verificación a nivel de datos
app.get('/api/profile', (req, res) => {
  // Solo el usuario puede ver su propio perfil
  const profile = db.users.findById(req.params.id);
  
  if (profile.userId !== req.user.id && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  res.json(profile);
});

Impacto Evitado:

  • Acceso a datos de otros usuarios
  • Manipulación de registros administrativos
  • Escalada de privilegios

5️⃣ Configuración de Seguridad Deficiente

El Problema:

// ❌ Errores comunes
1. Headers de seguridad faltantes
2. Librerías desactualizadas con vulnerabilidades
3. Debug mode activado en producción
4. Secretos en repositorio público
5. CORS demasiado permisivo

La Solución: Configuración Segura

// ✓ Express con helmet (headers de seguridad)
const helmet = require('helmet');
app.use(helmet());

// Expande manualmente
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "trusted-cdn.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
  },
}));

// ✓ CORS seguro
const cors = require('cors');
app.use(cors({
  origin: ['https://example.com', 'https://app.example.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

// ❌ Mal:
app.use(cors()); // Permite CUALQUIER origen

// ✓ Gestión de secretos
require('dotenv').config(); // Lee .env (NUNCA en git)

// En .env (gitignored)
DATABASE_URL=postgresql://user:pass@host/db
JWT_SECRET=super_secret_key_muy_segura
API_KEY=clave_api_produccion

// ✓ Nunca:
const secret = "hardcoded-secret"; // ❌
console.log(process.env.DATABASE_URL); // ❌ No imprimas secretos

Impacto Evitado:

  • Exposición de secretos
  • Acceso a través de vulnerabilidades conocidas
  • Information disclosure

Protección Contra Ataques Específicos

CSRF (Cross-Site Request Forgery)

// ✓ Generar token CSRF
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: false });

app.get('/form', csrfProtection, (req, res) => {
  res.send(`<form method="POST" action="/submit">
    <input type="hidden" name="_csrf" value="${req.csrfToken()}">
    <input type="text" name="data">
    <button type="submit">Submit</button>
  </form>`);
});

app.post('/submit', csrfProtection, (req, res) => {
  // Token verificado automáticamente
  res.json({ success: true });
});

Rate Limiting y DDoS

// ✓ Limitar requests
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // 100 requests
  message: 'Demasiadas requests, intenta después',
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', apiLimiter);

// ✓ Store en Redis para múltiples servidores
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient();

const limiter = rateLimit({
  store: new RedisStore({
    client: client,
    prefix: 'rl:', // rate limit
  }),
  windowMs: 15 * 60 * 1000,
  max: 100,
});

Validación de Input

// ✓ Usar librerías de validación
const { body, validationResult } = require('express-validator');

app.post('/register', [
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 12 }).trim(),
  body('age').isInt({ min: 18, max: 120 }),
  body('username').matches(/^[a-zA-Z0-9_]{3,20}$/),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // Datos validados
  register(req.body);
});

Checklist de Seguridad para Producción

  • Todas las dependencias actualizadas
  • Contraseñas con bcrypt/argon2
  • Rate limiting en endpoints públicos
  • Headers de seguridad (CSP, HSTS, etc.)
  • CORS configurado restrictivamente
  • Validación de input en backend
  • Sanitización de output
  • Logging de acciones críticas
  • Secrets en variables de entorno
  • HTTPS obligatorio
  • JWT o sesiones seguras
  • 2FA en cuentas administrativas
  • Backup regular
  • Monitoreo de seguridad
  • Plan de respuesta a incidentes

Herramientas Esenciales

# Escaneo de vulnerabilidades
npm audit
npm audit fix

# Análisis estático de seguridad
npm install -g snyk
snyk test

# OWASP Dependency Check
npm install -g @snyk/cli

# Escaneo de secretos
npm install -g git-secrets
git secrets --scan

Conclusión

La seguridad no es un complemento, es arquitectura fundamental. El 95% de los ataques exitosos aprovechan vulnerabilidades conocidas y prevenibles.

Los desarrolladores modernos deben ser guardianes de la seguridad. No puedes delegar esta responsabilidad.


Acción inmediata:

  1. Audita tu aplicación actual
  2. Implementa los 5 cambios más críticos
  3. Configura monitoreo de vulnerabilidades
  4. Entrena a tu equipo en OWASP Top 10

Recursos: