A medida que las APIs se convierten en el núcleo de productos y plataformas, su superficie de ataque crece. No alcanza con un simple token: hoy necesitas OAuth2 bien implementado, GraphQL protegido, mitigación DDoS, un WAF básico y monitoreo en tiempo real.
En este artículo construimos una arquitectura de defensa en profundidad, con ejemplos de código en Node.js que puedes adaptar a tus proyectos. Además, se corrigen y completan fragmentos para que sean más consistentes y robustos.
1. Seguridad OAuth2 avanzada
OAuth2 es el estándar para delegar acceso seguro a recursos, pero su implementación incompleta suele abrir puertas a ataques. Aquí combinamos Authorization Code + PKCE, validación estricta de cliente, expiración de tokens y un flujo client credentials para machine-to-machine.
1.1 Implementación completa de OAuth2 con PKCE
El siguiente servidor OAuth2 simplificado incluye:
- Generación de
code_verifierycode_challenge(PKCE). - Endpoint de autorización con validación de cliente, redirect URI y PKCE.
- Endpoint de token con validación de código, PKCE, redirect URI y expiración.
- Middleware para validar
access_tokenen rutas protegidas.
const express = require('express');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const app = express();
class OAuth2Server {
constructor() {
this.authorizationCodes = new Map();
this.accessTokens = new Map();
this.refreshTokens = new Map();
this.clients = new Map();
}
// Generar code_verifier para PKCE
generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
// Generar code_challenge (S256)
generateCodeChallenge(codeVerifier) {
return crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
}
// Endpoint de autorización
authorizationEndpoint(req, res) {
const {
client_id,
redirect_uri,
response_type,
scope,
state,
code_challenge,
code_challenge_method
} = req.query;
if (response_type !== 'code') {
return res.status(400).json({
error: 'unsupported_response_type',
error_description: 'Solo se soporta response_type=code'
});
}
// Validar cliente
const client = this.clients.get(client_id);
if (!client) {
return res.status(400).json({
error: 'invalid_client',
error_description: 'Cliente no encontrado'
});
}
// Validar redirect URI
if (!client.redirect_uris.includes(redirect_uri)) {
return res.status(400).json({
error: 'invalid_redirect_uri',
error_description: 'URI de redirección no válida'
});
}
// Validar PKCE (opcional pero recomendado)
if (code_challenge && !['plain', 'S256'].includes(code_challenge_method)) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Método de challenge no soportado'
});
}
// Simular página de login
if (!req.session || !req.session.user) {
const query = new URLSearchParams(req.query).toString();
return res.redirect(`/login?${query}`);
}
const user = req.session.user;
const authCode = uuidv4();
// Guardar código de autorización
this.authorizationCodes.set(authCode, {
client_id,
redirect_uri,
user_id: user.id,
scope: scope ? scope.split(' ') : [],
code_challenge,
code_challenge_method,
expires_at: Date.now() + 10 * 60 * 1000 // 10 minutos
});
// Redirigir con código
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', authCode);
if (state) redirectUrl.searchParams.set('state', state);
return res.redirect(redirectUrl.toString());
}
// Endpoint de token
async tokenEndpoint(req, res) {
const {
grant_type,
code,
redirect_uri,
client_id,
client_secret,
code_verifier
} = req.body;
try {
// Validar grant type
if (grant_type !== 'authorization_code') {
return res.status(400).json({
error: 'unsupported_grant_type',
error_description: 'Tipo de grant no soportado'
});
}
// Validar credenciales del cliente
const client = this.validateClient(client_id, client_secret);
if (!client) {
return res.status(401).json({
error: 'invalid_client',
error_description: 'Credenciales de cliente inválidas'
});
}
// Buscar código de autorización
const authCodeData = this.authorizationCodes.get(code);
if (!authCodeData) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Código de autorización inválido'
});
}
// Verificar expiración
if (Date.now() > authCodeData.expires_at) {
this.authorizationCodes.delete(code);
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Código de autorización expirado'
});
}
// Validar redirect URI
if (authCodeData.redirect_uri !== redirect_uri) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Redirect URI no coincide'
});
}
// Validar PKCE si se usó en la autorización
if (authCodeData.code_challenge) {
if (!code_verifier) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'Code verifier requerido'
});
}
const expectedChallenge =
authCodeData.code_challenge_method === 'S256'
? this.generateCodeChallenge(code_verifier)
: code_verifier;
if (authCodeData.code_challenge !== expectedChallenge) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Code verifier inválido'
});
}
}
// Generar tokens
const accessToken = this.generateToken();
const refreshToken = this.generateToken();
// Guardar access token
this.accessTokens.set(accessToken, {
client_id: authCodeData.client_id,
user_id: authCodeData.user_id,
scope: authCodeData.scope,
expires_at: Date.now() + 60 * 60 * 1000 // 1 hora
});
// Guardar refresh token
this.refreshTokens.set(refreshToken, {
client_id: authCodeData.client_id,
user_id: authCodeData.user_id,
scope: authCodeData.scope
});
// Limpiar código de autorización (one-time use)
this.authorizationCodes.delete(code);
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
refresh_token: refreshToken,
scope: authCodeData.scope.join(' ')
});
} catch (error) {
console.error('Error en token endpoint:', error);
return res.status(500).json({
error: 'server_error',
error_description: 'Error interno del servidor'
});
}
}
// Middleware de validación de token
validateToken(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token de acceso requerido'
});
}
const token = authHeader.substring(7);
const tokenData = this.accessTokens.get(token);
if (!tokenData) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token de acceso inválido'
});
}
if (Date.now() > tokenData.expires_at) {
this.accessTokens.delete(token);
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token de acceso expirado'
});
}
req.oauth = {
client_id: tokenData.client_id,
user_id: tokenData.user_id,
scope: tokenData.scope
};
return next();
}
validateClient(clientId, clientSecret) {
const client = this.clients.get(clientId);
return client && client.client_secret === clientSecret ? client : null;
}
generateToken() {
return crypto.randomBytes(32).toString('hex');
}
}
// Uso del servidor OAuth2
const oauthServer = new OAuth2Server();
// Registrar cliente de ejemplo
oauthServer.clients.set('my-client-id', {
client_id: 'my-client-id',
client_secret: 'my-client-secret',
redirect_uris: ['https://myapp.com/callback'],
scope: ['read', 'write']
});
// Middlewares globales
app.use(express.json());
// Rutas OAuth2
app.get('/oauth/authorize', (req, res) => {
oauthServer.authorizationEndpoint(req, res);
});
app.post('/oauth/token', (req, res) => {
oauthServer.tokenEndpoint(req, res);
});
// Ruta protegida con OAuth2
app.get(
'/api/protected',
(req, res, next) => oauthServer.validateToken(req, res, next),
(req, res) => {
res.json({
message: 'Acceso concedido a recurso protegido',
user_id: req.oauth.user_id,
scope: req.oauth.scope
});
}
);
1.2 Flujo Client Credentials para machine-to-machine
El flujo de client credentials permite que dos servicios se autentiquen entre sí sin usuario humano. Es clave para microservicios y jobs backend.
class ClientCredentialsFlow {
async clientCredentialsToken(req, res) {
const { grant_type, client_id, client_secret, scope } = req.body;
if (grant_type !== 'client_credentials') {
return res.status(400).json({
error: 'unsupported_grant_type'
});
}
// Validar cliente
const client = oauthServer.validateClient(client_id, client_secret);
if (!client) {
return res.status(401).json({
error: 'invalid_client'
});
}
// Generar token de acceso
const accessToken = oauthServer.generateToken();
const requestedScope = scope ? scope.split(' ') : client.scope;
// Verificar scopes solicitados
const validScope = requestedScope.every(s => client.scope.includes(s));
if (!validScope) {
return res.status(400).json({
error: 'invalid_scope'
});
}
oauthServer.accessTokens.set(accessToken, {
client_id: client.client_id,
user_id: null, // No hay usuario en este flujo
scope: requestedScope,
expires_at: Date.now() + 60 * 60 * 1000
});
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: requestedScope.join(' ')
});
}
}
// Ejemplo de ruta para client credentials
const clientCredentialsFlow = new ClientCredentialsFlow();
app.post('/oauth/client-token', (req, res) => {
clientCredentialsFlow.clientCredentialsToken(req, res);
});
2. Seguridad avanzada en GraphQL
GraphQL es potente pero fácil de abusar: queries profundas, introspección agresiva, ataques de complejidad, etc. Aquí combinamos límites de profundidad, complejidad, permisos por campo y validaciones adicionales.
2.1 Protección completa de servidor GraphQL
const { ApolloServer, gql } = require('apollo-server-express');
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
// La directiva rateLimit se puede integrar con herramientas externas
// const { rateLimitDirective } = require('graphql-rate-limit-directive');
class SecureGraphQLServer {
constructor() {
this.typeDefs = gql`
directive @rateLimit(
max: Int
window: String
message: String
) on FIELD_DEFINITION
type Query {
users(limit: Int = 10): [User] @rateLimit(max: 10, window: "1m")
user(id: ID!): User
posts(limit: Int = 10): [Post]
}
type Mutation {
createPost(input: PostInput!): Post @rateLimit(max: 5, window: "5m")
deletePost(id: ID!): Boolean
}
type User {
id: ID!
email: String!
posts: [Post]
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
input PostInput {
title: String!
content: String!
}
`;
this.resolvers = {
Query: {
users: async (_, { limit }, context) => {
await this.authorize(context.user, ['users:read']);
return this.getUsers(limit);
},
user: async (_, { id }, context) => {
await this.authorize(context.user, ['users:read']);
return this.getUser(id);
},
posts: async (_, { limit }, context) => {
await this.authorize(context.user, ['posts:read']);
return this.getPosts(limit);
}
},
Mutation: {
createPost: async (_, { input }, context) => {
await this.authorize(context.user, ['posts:write']);
return this.createPost(input, context.user);
},
deletePost: async (_, { id }, context) => {
await this.authorize(context.user, ['posts:delete']);
return this.deletePost(id, context.user);
}
},
User: {
posts: (user, _, context) => {
// Solo permite ver posts propios a menos que sea admin
if (
context.user.id !== user.id &&
!context.user.roles.includes('admin')
) {
return [];
}
return this.getUserPosts(user.id);
}
}
};
}
createServer() {
// Reglas de validación de seguridad
const validationRules = [
depthLimit(10), // Limitar profundidad máxima
createComplexityLimitRule(1000, {
onCost: cost => {
console.log('Query complexity:', cost);
},
formatErrorMessage: cost =>
`Query demasiado compleja: ${cost}. Máximo permitido: 1000`
})
];
return new ApolloServer({
typeDefs: this.typeDefs,
resolvers: this.resolvers,
validationRules,
context: ({ req }) => this.createContext(req),
plugins: [this.securityPlugin()],
formatError: err => this.formatError(err)
});
}
async createContext(req) {
// Autenticación JWT (implementación real pendiente)
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.replace('Bearer ', '')
: null;
let user = null;
if (token) {
try {
user = await this.verifyJWT(token);
} catch (error) {
console.warn('Token JWT inválido:', error.message);
}
}
return {
user,
ip: req.ip,
userAgent: req.get('User-Agent')
};
}
securityPlugin() {
return {
requestDidStart: () => ({
didResolveOperation: async requestContext => {
await this.validateQuery(requestContext);
},
didEncounterErrors: async requestContext => {
this.logSecurityEvents(requestContext);
}
})
};
}
async validateQuery(requestContext) {
const { request, context } = requestContext;
const query = request.query || '';
// Validar queries recursivas
this.preventRecursiveQueries(query);
// Validar número de campos
this.validateFieldLimits(query);
// Log de queries sospechosas
if (this.isSuspiciousQuery(query)) {
await this.logSuspiciousActivity(context, query);
}
}
preventRecursiveQueries(query) {
const recursivePatterns = [
/user\s*{\s*posts\s*{\s*author\s*{\s*posts/gi,
/posts\s*{\s*author\s*{\s*posts\s*{\s*author/gi
];
for (const pattern of recursivePatterns) {
if (pattern.test(query)) {
throw new Error('Query recursiva detectada y bloqueada');
}
}
}
validateFieldLimits(query) {
const fieldCount = (query.match(/\w+\s*{/g) || []).length;
if (fieldCount > 50) {
throw new Error(
'Demasiados campos solicitados en la query. Límite: 50 campos'
);
}
}
isSuspiciousQuery(query) {
const suspiciousPatterns = [
/__schema|__type|__typename/gi, // Introspección abusiva
/(\w+)\s*{\s*\1/gi, // Auto-referencia
/\.\.\./g // Fragmentos excesivos
];
return suspiciousPatterns.some(pattern => pattern.test(query));
}
async authorize(user, requiredPermissions) {
if (!user) {
throw new Error('Authentication required');
}
const hasPermission = requiredPermissions.every(permission =>
user.permissions.includes(permission)
);
if (!hasPermission) {
throw new Error('Insufficient permissions');
}
}
formatError(err) {
const { message, locations, path } = err;
if (message.includes('Database') || message.includes('Internal')) {
console.error('Error interno:', err);
return {
message: 'Internal server error',
code: 'INTERNAL_ERROR'
};
}
return {
message,
locations,
path,
code: err.extensions?.code || 'GRAPHQL_ERROR'
};
}
// Métodos de dominio (stubs de ejemplo)
async verifyJWT(token) {
// Aquí iría la verificación real del JWT
return {
id: 'user-123',
roles: ['user'],
permissions: ['users:read', 'posts:read']
};
}
async getUsers(limit) {
return [];
}
async getUser(id) {
return null;
}
async getPosts(limit) {
return [];
}
async createPost(input, user) {
return { id: 'post-1', title: input.title, content: input.content, author: user };
}
async deletePost(id, user) {
return true;
}
async getUserPosts(userId) {
return [];
}
logSecurityEvents(requestContext) {
console.warn('Errores GraphQL:', requestContext.errors);
}
async logSuspiciousActivity(context, query) {
console.warn('Query sospechosa detectada:', {
ip: context.ip,
user: context.user?.id,
query
});
}
}
// Inicialización segura del servidor GraphQL
async function startSecureGraphQL(app) {
const secureGraphQL = new SecureGraphQLServer();
const server = secureGraphQL.createServer();
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
}
2.2 Rate limiting avanzado para GraphQL
Más allá de la complejidad, es útil limitar la frecuencia de llamadas a ciertos campos. Usamos Redis para contar peticiones por usuario/IP, operación y campo.
class GraphQLRateLimiter {
constructor(redisClient) {
this.redis = redisClient;
this.limits = {
Query: {
users: { window: '1m', max: 10 },
posts: { window: '1m', max: 20 }
},
Mutation: {
createPost: { window: '5m', max: 5 },
deletePost: { window: '1h', max: 10 }
}
};
}
async checkRateLimit(context, fieldName, operation) {
const limitConfig = this.limits[operation]?.[fieldName];
if (!limitConfig) return;
const key = `graphql_rate_limit:${context.user?.id || context.ip}:${operation}:${fieldName}`;
const now = Date.now();
const windowMs = this.parseWindow(limitConfig.window);
const pipeline = this.redis.pipeline();
pipeline.zremrangebyscore(key, 0, now - windowMs);
pipeline.zadd(key, now, now);
pipeline.zcard(key);
pipeline.expire(key, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const requestCount = results[2][1];
if (requestCount > limitConfig.max) {
throw new Error(
`Rate limit exceeded for ${fieldName}. ` +
`Maximum ${limitConfig.max} requests per ${limitConfig.window}`
);
}
return {
limit: limitConfig.max,
remaining: Math.max(0, limitConfig.max - requestCount),
reset: now + windowMs
};
}
parseWindow(window) {
const units = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000
};
const match = window.match(/^(\d+)([smhd])$/);
if (!match) return 60000; // Default 1 minuto
const [, amount, unit] = match;
return parseInt(amount, 10) * units[unit];
}
}
// Uso simple dentro de resolvers
const graphQLLimiter = new GraphQLRateLimiter(redisClient);
const resolvers = {
Query: {
users: async (_, { limit }, context, info) => {
await graphQLLimiter.checkRateLimit(
context,
info.fieldName,
info.parentType.name
);
// Implementación real del resolver
return [];
}
}
};
3. Protección avanzada contra DDoS
Ninguna API está exenta de ataques de denegación de servicio. Podemos mitigar gran parte con:
- Rate limiting por IP, usuario y endpoint.
- Detección de ráfagas de tráfico y patrones extraños.
- Bloqueo temporal y retos (challenges) para tráfico sospechoso.
3.1 Sistema de mitigación DDoS en capas
const crypto = require('crypto');
class DDoSProtectionSystem {
constructor(redisClient) {
this.redis = redisClient;
this.thresholds = {
IP: { requests: 100, window: 60000 }, // 100 req/min por IP
USER: { requests: 1000, window: 60000 }, // 1000 req/min por usuario
ENDPOINT: { requests: 500, window: 60000 } // 500 req/min por endpoint
};
}
async checkDDoSPotential(req) {
const ip = req.ip;
const userId = req.user?.id || null;
const endpoint = req.path;
const checks = [
this.checkIPRate(ip),
this.checkUserRate(userId),
this.checkEndpointRate(endpoint, ip),
this.checkBehaviorPatterns(req),
this.checkIPReputation(ip)
];
const results = await Promise.all(checks);
const isUnderAttack = results.some(result => result.isSuspicious);
if (isUnderAttack) {
await this.triggerMitigation(req, results);
return false;
}
return true;
}
async checkIPRate(ip) {
const key = `ddos:ip:${ip}`;
const now = Date.now();
const window = this.thresholds.IP.window;
const requestCount = await this.redis.zcount(key, now - window, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, Math.ceil(window / 1000));
return {
type: 'IP_RATE',
isSuspicious: requestCount > this.thresholds.IP.requests,
severity: Math.min(requestCount / this.thresholds.IP.requests, 10),
details: { ip, requestCount, threshold: this.thresholds.IP.requests }
};
}
async checkUserRate(userId) {
if (!userId) {
return {
type: 'USER_RATE',
isSuspicious: false,
severity: 0,
details: {}
};
}
const key = `ddos:user:${userId}`;
const now = Date.now();
const window = this.thresholds.USER.window;
const requestCount = await this.redis.zcount(key, now - window, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, Math.ceil(window / 1000));
return {
type: 'USER_RATE',
isSuspicious: requestCount > this.thresholds.USER.requests,
severity: Math.min(requestCount / this.thresholds.USER.requests, 10),
details: { userId, requestCount, threshold: this.thresholds.USER.requests }
};
}
async checkEndpointRate(endpoint, ip) {
const key = `ddos:endpoint:${endpoint}:${ip}`;
const now = Date.now();
const window = this.thresholds.ENDPOINT.window;
const requestCount = await this.redis.zcount(key, now - window, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, Math.ceil(window / 1000));
return {
type: 'ENDPOINT_RATE',
isSuspicious: requestCount > this.thresholds.ENDPOINT.requests,
severity: Math.min(requestCount / this.thresholds.ENDPOINT.requests, 10),
details: { endpoint, requestCount, threshold: this.thresholds.ENDPOINT.requests }
};
}
async checkBehaviorPatterns(req) {
const patternChecks = [
this.checkUserAgent(req.get('User-Agent')),
this.checkRequestFrequency(req),
this.checkGeolocationAnomaly(req.ip),
this.checkURLPatterns(req.path)
];
const results = await Promise.all(patternChecks);
const suspiciousCount = results.filter(r => r.isSuspicious).length;
return {
type: 'BEHAVIOR_PATTERN',
isSuspicious: suspiciousCount >= 2,
severity: suspiciousCount / 4,
details: { patterns: results }
};
}
checkUserAgent(userAgent) {
const suspiciousUAs = [
'bot', 'crawler', 'scraper', 'python', 'curl', 'wget',
'mass', 'scan', 'hack', 'attack'
];
const isSuspicious = suspiciousUAs.some(term =>
userAgent?.toLowerCase().includes(term)
);
return { pattern: 'USER_AGENT', isSuspicious, details: { userAgent } };
}
async checkRequestFrequency(req) {
const key = `ddos:burst:${req.ip}`;
const now = Date.now();
const burstWindow = 1000; // 1 segundo
const recentRequests = await this.redis.zcount(key, now - burstWindow, now);
await this.redis.zadd(key, now, now);
await this.redis.expire(key, 2);
return {
pattern: 'REQUEST_BURST',
isSuspicious: recentRequests > 10, // Más de 10 req/segundo
details: { recentRequests }
};
}
async checkGeolocationAnomaly(ip) {
// Placeholder: aquí iría integración con un servicio de geolocalización
return { pattern: 'GEO_ANOMALY', isSuspicious: false, details: { ip } };
}
checkURLPatterns(path) {
const suspiciousPatterns = [
/\.\.\//, // Path traversal
/\/\.env/, // Archivos de configuración
/\/phpmyadmin/, // Herramientas de administración
/\/wp-admin/, // WordPress admin
/\/api\/[^/]+\/\.\./ // API path traversal
];
const isSuspicious = suspiciousPatterns.some(pattern => pattern.test(path));
return { pattern: 'URL_PATTERN', isSuspicious, details: { path } };
}
async checkIPReputation(ip) {
// Placeholder: integración con servicio de reputación de IP
return { type: 'IP_REPUTATION', isSuspicious: false, severity: 0, details: { ip } };
}
async triggerMitigation(req, alerts) {
const ip = req.ip;
const suspiciousAlerts = alerts.filter(a => a.isSuspicious);
const severity = Math.max(...suspiciousAlerts.map(a => a.severity || 0), 0);
console.warn(`🚨 Activando mitigación DDoS para IP: ${ip}`, {
severity,
alerts: suspiciousAlerts
});
if (severity < 3) {
await this.enforceStrictRateLimit(ip);
} else if (severity < 7) {
await this.requireChallenge(ip);
} else {
await this.temporaryBlock(ip);
}
await this.alertSecurityTeam(req, suspiciousAlerts);
}
async enforceStrictRateLimit(ip) {
const key = `ddos:strict_limit:${ip}`;
await this.redis.setex(key, 300, 'enforced'); // 5 minutos
}
async requireChallenge(ip) {
const key = `ddos:challenge:${ip}`;
const challenge = crypto.randomBytes(8).toString('hex');
await this.redis.setex(key, 600, challenge); // 10 minutos
}
async temporaryBlock(ip) {
const key = `ddos:blocked:${ip}`;
await this.redis.setex(key, 1800, 'blocked'); // 30 minutos
await this.redis.sadd('ddos:blacklist', ip);
await this.redis.expire('ddos:blacklist', 1800);
}
async isIPBlocked(ip) {
const exists = await this.redis.exists(`ddos:blocked:${ip}`);
return Boolean(exists);
}
async alertSecurityTeam(req, alerts) {
// Aquí podrías enviar una notificación a Slack/Email/SIEM
console.log('Notificando equipo de seguridad:', {
ip: req.ip,
alerts
});
}
}
// Middleware de protección DDoS
const ddosProtection = new DDoSProtectionSystem(redisClient);
app.use(async (req, res, next) => {
if (await ddosProtection.isIPBlocked(req.ip)) {
return res.status(429).json({
error: 'IP temporalmente bloqueada por actividad sospechosa',
code: 'IP_BLOCKED',
retryAfter: 1800
});
}
const allowed = await ddosProtection.checkDDoSPotential(req);
if (!allowed) {
return res.status(429).json({
error: 'Actividad sospechosa detectada',
code: 'SUSPICIOUS_ACTIVITY'
});
}
return next();
});
3.2 Web Application Firewall (WAF) básico
Un WAF ligero puede detectar patrones de SQLi, XSS, path traversal y otros intentos obvios antes de que lleguen a tu aplicación.
class SimpleWAF {
constructor() {
this.rules = [
{
name: 'SQL Injection',
pattern: /(\bUNION\b.*\bSELECT|\bDROP\b|\bINSERT\b|\bDELETE\b|\bUPDATE\b.*\bSET|\bOR\b.*=)/gi,
action: 'block'
},
{
name: 'XSS Attempt',
pattern: /(
4. Sistema de monitoreo y alertas de seguridad
De nada sirve bloquear si no ves lo que pasa. Un dashboard de seguridad te permite:
- Contar intentos de ataque, bloqueos y fallos de autenticación.
- Identificar IPs más problemáticas.
- Generar alertas cuando el riesgo se dispara.
4.1 Dashboard de seguridad en tiempo real
class SecurityDashboard {
constructor(redisClient) {
this.redis = redisClient;
this.metrics = {
totalRequests: 0,
blockedRequests: 0,
authenticationFailures: 0,
rateLimitHits: 0,
ddosAlerts: 0
};
}
async recordSecurityEvent(event) {
const timestamp = Date.now();
const eventKey = `security:events:${timestamp}`;
await this.redis.hset(eventKey, {
type: event.type,
severity: event.severity,
ip: event.ip,
user: event.user || 'anonymous',
endpoint: event.endpoint,
details: JSON.stringify(event.details || {}),
timestamp: timestamp
});
await this.redis.expire(eventKey, 24 * 60 * 60); // 24 horas
this.updateMetrics(event);
if (event.severity >= 7) {
await this.sendAlert(event);
}
}
updateMetrics(event) {
this.metrics.totalRequests += 1;
switch (event.type) {
case 'AUTH_FAILURE':
this.metrics.authenticationFailures += 1;
break;
case 'RATE_LIMIT':
this.metrics.rateLimitHits += 1;
break;
case 'DDoS_ALERT':
this.metrics.ddosAlerts += 1;
break;
case 'REQUEST_BLOCKED':
this.metrics.blockedRequests += 1;
break;
default:
break;
}
}
async sendAlert(event) {
const alert = {
title: `Alerta de Seguridad: ${event.type}`,
severity: event.severity,
ip: event.ip,
timestamp: new Date().toISOString(),
details: event.details
};
await this.sendToWebhook(alert);
await this.sendToLog(alert);
}
async sendToWebhook(alert) {
// Integración con Slack / Teams / Webhook
console.log('Enviando alerta a webhook:', alert.title);
}
async sendToLog(alert) {
// Integración con sistema de logs centralizado (ELK, Loki, etc.)
console.log('Registrando alerta en logs:', alert.title);
}
async getSecurityReport() {
const now = Date.now();
const oneHourAgo = now - 60 * 60 * 1000;
const eventKeys = await this.redis.keys('security:events:*');
const recentEvents = [];
for (const key of eventKeys) {
const timestamp = parseInt(key.split(':')[2], 10);
if (timestamp >= oneHourAgo) {
const event = await this.redis.hgetall(key);
recentEvents.push({
...event,
details: JSON.parse(event.details)
});
}
}
return {
metrics: this.metrics,
recentEvents: recentEvents.slice(-100),
topOffenders: await this.getTopOffenders(recentEvents),
riskAssessment: this.assessRisk(recentEvents)
};
}
async getTopOffenders(events) {
const ipCounts = {};
events.forEach(event => {
ipCounts[event.ip] = (ipCounts[event.ip] || 0) + 1;
});
return Object.entries(ipCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([ip, count]) => ({ ip, count }));
}
assessRisk(events) {
const highSeverityEvents = events.filter(e => e.severity >= 7);
const riskScore = Math.min(highSeverityEvents.length * 10, 100);
let level = 'LOW';
if (riskScore >= 70) level = 'HIGH';
else if (riskScore >= 30) level = 'MEDIUM';
return { score: riskScore, level };
}
}
// Endpoint de monitoreo de seguridad (protegido)
app.get(
'/admin/security-dashboard',
authenticateToken,
authorize(['admin', 'security']),
async (req, res) => {
const dashboard = new SecurityDashboard(redisClient);
const report = await dashboard.getSecurityReport();
return res.json(report);
}
);
Conclusión
La seguridad en APIs modernas no se resuelve con un único componente. Necesitas múltiples capas: OAuth2 bien implementado, GraphQL endurecido, rate limiting inteligente, protección DDoS, WAF y monitoreo continuo.
El código anterior no es un producto listo para producción, pero sí una base sólida para construir una arquitectura de defensa en profundidad y elevar drásticamente el nivel de seguridad de tus APIs.