← ← Volver a todas las notas

Seguridad avanzada en APIs: OAuth2, GraphQL y protección DDoS

2025-11-06 · Benja

Artículo técnico sobre seguridad avanzada en APIs: OAuth2 con PKCE, GraphQL endurecido, mitigación DDoS, WAF y monitoreo.

Seguridad avanzada en APIs: OAuth2, GraphQL y protección DDoS

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_verifier y code_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_token en 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.