Importancia de la Seguridad Web
Las aplicaciones web están constantemente expuestas a riesgos de ataques. OWASP (Open Web Application Security Project) publica el Top 10, que resume los riesgos de seguridad más críticos.
flowchart TB
subgraph OWASP["OWASP Top 10 2021"]
A1["1. Broken Access Control<br/>(Control de Acceso Deficiente)"]
A2["2. Cryptographic Failures<br/>(Fallas Criptográficas)"]
A3["3. Injection<br/>(Inyección)"]
A4["4. Insecure Design<br/>(Diseño Inseguro)"]
A5["5. Security Misconfiguration<br/>(Configuración Incorrecta)"]
A6["6. Vulnerable Components<br/>(Componentes Vulnerables)"]
A7["7. Authentication Failures<br/>(Fallas de Autenticación)"]
A8["8. Software Integrity Failures<br/>(Fallas de Integridad)"]
A9["9. Logging Failures<br/>(Fallas de Registro y Monitoreo)"]
A10["10. SSRF<br/>(Server-Side Request Forgery)"]
end
XSS (Cross-Site Scripting)
Tipos de Ataques
flowchart TB
subgraph XSS["Tipos de XSS"]
subgraph Reflected["1. Reflected XSS (Reflejado)"]
R1["URL: /search?q=script"] --> R2["HTML: Script insertado en resultados de búsqueda"]
end
subgraph Stored["2. Stored XSS (Almacenado)"]
S1["Publicar comentario"] --> S2["Guardado en BD"] --> S3["Mostrado a otros usuarios"]
Note1["El código de ataque permanece de forma persistente"]
end
subgraph DOM["3. DOM-based XSS"]
D1["JavaScript del lado cliente<br/>inserta parámetros URL directamente en el DOM"]
end
end
Medidas de Defensa
// 1. Escape HTML
function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
// 2. Usar escape automático del motor de plantillas
// React: JSX escapa automáticamente
<div>{userInput}</div> // Seguro
// Peligroso: evitar dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // ¡Peligroso!
// 3. Content Security Policy (CSP)
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
`.replace(/\n/g, ''),
},
];
// 4. Uso de Cookie HttpOnly
res.cookie('session', token, {
httpOnly: true, // Inaccesible desde JavaScript
secure: true, // Requiere HTTPS
sameSite: 'strict',
maxAge: 3600000,
});
CSRF (Cross-Site Request Forgery)
sequenceDiagram
participant U as Usuario
participant B as Sitio del Banco
participant A as Sitio del Atacante
Note over U,B: 1. Usuario inicia sesión en sitio legítimo
U->>B: Login
B->>U: Cookie de Sesión
Note over U,A: 2. Usuario accede al sitio trampa del atacante
U->>A: Acceso
A->>U: Formulario oculto<br/>(POST a bank.com/transfer)
Note over U,B: 3. Solicitud maliciosa ejecutada con sesión del usuario
U->>B: POST /transfer<br/>(to=attacker, amount=10000)
B-->>U: Transferencia completada
Medidas de Defensa
// 1. Token CSRF
import { randomBytes } from 'crypto';
function generateCsrfToken(): string {
return randomBytes(32).toString('hex');
}
// Guardar token en sesión
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCsrfToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// Incluir token en formulario
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value={csrfToken} />
...
</form>
// Verificar solicitud
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Ejecutar procesamiento
});
// 2. Cookie SameSite
res.cookie('session', token, {
sameSite: 'strict', // o 'lax'
httpOnly: true,
secure: true,
});
// 3. Verificación del encabezado Origin
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://myapp.com'];
if (req.method !== 'GET' && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
});
Inyección SQL
Código Vulnerable
// Ejemplo peligroso
app.get('/users', async (req, res) => {
const { name } = req.query;
// ¡Posible inyección SQL!
const result = await db.query(`SELECT * FROM users WHERE name = '${name}'`);
res.json(result);
});
// Ejemplo de ataque: ?name=' OR '1'='1
// SELECT * FROM users WHERE name = '' OR '1'='1'
// → Se obtienen todos los usuarios
Medidas de Defensa
// 1. Prepared Statements (Consultas Parametrizadas)
app.get('/users', async (req, res) => {
const { name } = req.query;
const result = await db.query(
'SELECT * FROM users WHERE name = $1',
[name]
);
res.json(result);
});
// 2. Uso de ORM
// Prisma
const users = await prisma.user.findMany({
where: { name },
});
// Drizzle
const users = await db.select().from(usersTable).where(eq(usersTable.name, name));
// 3. Validación de entrada
import { z } from 'zod';
const QuerySchema = z.object({
name: z.string().max(100).regex(/^[a-zA-Z0-9\s]+$/),
});
app.get('/users', async (req, res) => {
const result = QuerySchema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({ error: 'Invalid input' });
}
// ...
});
Seguridad de Autenticación
// Hash seguro de contraseñas
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Uso seguro de JWT
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '15m';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
function generateTokens(userId: string) {
const accessToken = jwt.sign({ userId }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
algorithm: 'HS256',
});
const refreshToken = jwt.sign({ userId, type: 'refresh' }, JWT_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
algorithm: 'HS256',
});
return { accessToken, refreshToken };
}
// Protección contra fuerza bruta
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // hasta 5 intentos
message: { error: 'Too many login attempts' },
standardHeaders: true,
legacyHeaders: false,
});
app.post('/login', loginLimiter, async (req, res) => {
// Procesamiento de login
});
Encabezados de Seguridad
// middleware/security.ts
import helmet from 'helmet';
app.use(helmet());
// O configuración individual
app.use((req, res, next) => {
// Filtro XSS
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevenir Content-Type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevenir clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Forzar HTTPS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Política de referrer
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Política de permisos
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
Validación de Entrada
import { z } from 'zod';
import DOMPurify from 'dompurify';
// Definición de esquema
const UserInputSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(2).max(100).regex(/^[\p{L}\s]+$/u),
bio: z.string().max(500).optional(),
website: z.string().url().optional(),
});
// Sanitización
function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
}
// Validación de carga de archivos
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
function validateFile(file: File): boolean {
if (file.size > MAX_FILE_SIZE) {
throw new Error('File too large');
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error('Invalid file type');
}
return true;
}
Lista de Verificación de Seguridad
| Categoría | Elemento a Verificar |
|---|---|
| Autenticación | Hash de contraseñas (bcrypt/Argon2) |
| Autenticación | Protección contra fijación de sesión |
| Autenticación | Soporte MFA |
| Entrada | Validación de todas las entradas |
| Entrada | Consultas SQL parametrizadas |
| Salida | Escape HTML |
| Comunicación | HTTPS obligatorio |
| Cookie | HttpOnly, Secure, SameSite |
| Encabezados | Configuración CSP, HSTS |
| Dependencias | Escaneo de vulnerabilidades (npm audit) |