Missing Rate Limiting in APIs Enabling Denial of Service Attacks

High Risk API Security
apirate-limitingdosbrute-forceresource-exhaustionperformancethrottling

What it is

A high-severity vulnerability where APIs lack proper rate limiting controls, allowing attackers to overwhelm services through excessive requests. This can lead to denial of service (DoS), resource exhaustion, brute force attacks, and degraded performance for legitimate users. Without rate limiting, attackers can abuse expensive operations, exhaust database connections, consume bandwidth, and potentially cause complete service outages.

// VULNERABLE: No rate limiting on critical endpoints const express = require('express'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const app = express(); // Authentication endpoint without rate limiting app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; try { const user = await User.findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } // Expensive bcrypt operation const isValid = await bcrypt.compare(password, user.password); if (!isValid) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET); res.json({ token, user: { id: user.id, email: user.email } }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } }); // Password reset without rate limiting app.post('/api/auth/reset-password', async (req, res) => { const { email } = req.body; const user = await User.findOne({ email }); if (user) { // Expensive email operation await sendPasswordResetEmail(user.email); } // Always return success to prevent email enumeration res.json({ message: 'If email exists, reset link sent' }); }); // File upload without limits app.post('/api/files/upload', upload.single('file'), (req, res) => { // Process file upload const processedFile = processFile(req.file); res.json({ fileId: processedFile.id }); });
// SECURE: Comprehensive rate limiting implementation const express = require('express'); const rateLimit = require('express-rate-limit'); const slowDown = require('express-slow-down'); const RedisStore = require('rate-limit-redis'); const redis = require('redis'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const app = express(); const redisClient = redis.createClient(process.env.REDIS_URL); // Progressive delay for suspicious behavior const speedLimiter = slowDown({ store: new RedisStore({ client: redisClient, prefix: 'speed_limit:' }), windowMs: 15 * 60 * 1000, // 15 minutes delayAfter: 5, // Allow 5 requests per window at full speed delayMs: 500, // Add 500ms delay per request after delayAfter maxDelayMs: 20000, // Maximum delay of 20 seconds }); // Strict rate limiting for authentication const authLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'auth_limit:' }), windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window message: { error: 'Too many authentication attempts', retryAfter: 900 // 15 minutes in seconds }, standardHeaders: true, keyGenerator: (req) => { // Combine IP and email for more granular limiting return `${req.ip}:${req.body.email || 'anonymous'}`; }, skip: (req) => { // Skip rate limiting for successful authentications return req.skipRateLimit === true; } }); // Rate limiting for password reset const resetLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'reset_limit:' }), windowMs: 60 * 60 * 1000, // 1 hour max: 3, // 3 reset attempts per hour per email keyGenerator: (req) => req.body.email, message: { error: 'Too many password reset attempts' } }); // File upload rate limiting const uploadLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'upload_limit:' }), windowMs: 60 * 1000, // 1 minute max: 10, // 10 uploads per minute message: { error: 'Upload rate limit exceeded' } }); // Apply middleware app.use('/api/auth', speedLimiter); app.post('/api/auth/login', authLimiter, async (req, res) => { const { email, password } = req.body; try { const user = await User.findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } const isValid = await bcrypt.compare(password, user.password); if (!isValid) { return res.status(401).json({ error: 'Invalid credentials' }); } // Skip rate limiting for successful authentication req.skipRateLimit = true; const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET); res.json({ token, user: { id: user.id, email: user.email } }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } }); app.post('/api/auth/reset-password', resetLimiter, async (req, res) => { const { email } = req.body; const user = await User.findOne({ email }); if (user) { await sendPasswordResetEmail(user.email); } res.json({ message: 'If email exists, reset link sent' }); }); app.post('/api/files/upload', uploadLimiter, upload.single('file'), (req, res) => { const processedFile = processFile(req.file); res.json({ fileId: processedFile.id }); });

💡 Why This Fix Works

The secure implementation adds comprehensive rate limiting with Redis-backed storage for distributed applications. It includes progressive delays, different limits for different endpoints, and smart key generation that considers both IP and user context. The rate limiting is applied at the middleware level and includes proper error messages with retry information.

Why it happens

REST APIs without any rate limiting implementation are vulnerable to abuse. Attackers can send unlimited requests to expensive endpoints like search, authentication, or data processing operations, causing server overload and service degradation.

Root causes

Unprotected REST API Endpoints

REST APIs without any rate limiting implementation are vulnerable to abuse. Attackers can send unlimited requests to expensive endpoints like search, authentication, or data processing operations, causing server overload and service degradation.

Preview example – JAVASCRIPT
// VULNERABLE: Express.js API without rate limiting
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();

// No rate limiting on authentication endpoint
app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;
    
    // Expensive bcrypt operation with no protection
    const user = await User.findOne({ username });
    if (user && await bcrypt.compare(password, user.passwordHash)) {
        res.json({ token: generateToken(user) });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});

// Expensive search endpoint without limits
app.get('/api/search', async (req, res) => {
    const { query } = req.query;
    
    // Unprotected database query that could be expensive
    const results = await Product.find({
        $text: { $search: query }
    }).limit(100);
    
    res.json(results);
});

// Attacker can make unlimited requests:
// POST /api/login (brute force attempts)
// GET /api/search?query=complex-search (resource exhaustion)

GraphQL APIs Without Query Complexity Limits

GraphQL APIs without rate limiting or query complexity analysis are particularly vulnerable to abuse. Attackers can craft deeply nested queries or request large datasets in a single query, consuming significant server resources and potentially causing outages.

Preview example – JAVASCRIPT
// VULNERABLE: Apollo GraphQL server without rate limiting
const { ApolloServer, gql } = require('apollo-server-express');

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    posts: [Post!]!
    followers: [User!]!
  }
  
  type Post {
    id: ID!
    title: String!
    author: User!
    comments: [Comment!]!
  }
  
  type Query {
    users: [User!]!
    searchPosts(query: String!): [Post!]!
  }
`;

const resolvers = {
  Query: {
    // No rate limiting on expensive operations
    users: () => User.find().populate('posts').populate('followers'),
    searchPosts: (_, { query }) => {
      // Expensive text search without limits
      return Post.find({ $text: { $search: query } })
        .populate('author')
        .populate('comments');
    }
  }
};

// Attacker can send malicious queries:
// query { users { posts { comments { author { posts { comments } } } } } }
// Deeply nested query that exponentially increases data retrieval

Microservices Without Inter-Service Rate Limiting

In microservices architectures, lack of rate limiting between services can amplify DoS attacks. A compromised or misbehaving service can overwhelm other services, and external attacks can cascade through the entire system without proper throttling mechanisms.

Preview example – JAVA
// VULNERABLE: Spring Boot microservice without rate limiting
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private InventoryService inventoryService;
    
    // No rate limiting on expensive operation
    @PostMapping("/process")
    public ResponseEntity<Order> processOrder(@RequestBody Order order) {
        // Calls to other microservices without throttling
        PaymentResult payment = paymentService.processPayment(order.getPayment());
        
        // Multiple service calls per request
        for (OrderItem item : order.getItems()) {
            inventoryService.reserveItem(item.getProductId(), item.getQuantity());
        }
        
        // Expensive database operations
        Order savedOrder = orderRepository.save(order);
        
        // Email service call without limits
        emailService.sendOrderConfirmation(savedOrder);
        
        return ResponseEntity.ok(savedOrder);
    }
}

// Attack scenarios:
// 1. Flood /api/orders/process endpoint
// 2. Overwhelm payment and inventory services
// 3. Exhaust database connections
// 4. Spam email service

API Gateway Without Proper Throttling Configuration

API gateways configured without appropriate rate limiting policies or with overly permissive limits can fail to protect backend services. Misconfigured rate limiting rules, absence of per-user limits, or inadequate burst handling can allow attacks to reach protected services.

Preview example – YAML
# VULNERABLE: Kong API Gateway configuration without rate limiting
apiVersion: configuration.konghq.com/v1
kind: KongIngress
metadata:
  name: api-ingress
proxy:
  path: /api
  connect_timeout: 60000
  read_timeout: 60000
  write_timeout: 60000
# NO rate limiting plugin configured

---
# AWS API Gateway without throttling
Resources:
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: MyAPI
      Description: API without rate limiting
      
  ApiMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !GetAtt ApiGateway.RootResourceId
      HttpMethod: POST
      AuthorizationType: NONE
      # No throttling settings configured
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations
        
# Results in unlimited requests reaching backend services

Fixes

1

Implement Express.js Rate Limiting with Redis

Use rate limiting middleware like express-rate-limit with Redis store for distributed rate limiting. Configure different limits for different endpoints based on their computational cost and business requirements.

View implementation – JAVASCRIPT
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

const redisClient = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT
});

// Strict rate limiting for authentication endpoints
const authLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'auth_rl:'
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: {
    error: 'Too many authentication attempts, please try again later',
    retryAfter: '15 minutes'
  },
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => {
    // Rate limit by IP and username combination
    return `${req.ip}:${req.body.username || 'anonymous'}`;
  }
});

// General API rate limiting
const apiLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'api_rl:'
  }),
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
  message: {
    error: 'API rate limit exceeded',
    retryAfter: '1 minute'
  }
});

// Apply rate limiting
app.use('/api/', apiLimiter);
app.use('/api/login', authLimiter);
app.use('/api/register', authLimiter);
2

Configure GraphQL Query Complexity Analysis

Implement query complexity analysis and depth limiting in GraphQL to prevent resource exhaustion attacks. Use libraries like graphql-query-complexity and graphql-depth-limit to analyze and reject overly complex queries.

View implementation – JAVASCRIPT
const { ApolloServer } = require('apollo-server-express');
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity').costAnalysisValidator;
const rateLimit = require('express-rate-limit');

// GraphQL-specific rate limiting
const graphqlLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 50, // 50 queries per minute
  keyGenerator: (req) => {
    // Rate limit by user ID if authenticated, otherwise by IP
    return req.user?.id || req.ip;
  }
});

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // Limit query depth to prevent deeply nested queries
    depthLimit(10),
    
    // Analyze query complexity and reject expensive queries
    costAnalysis({
      maximumCost: 1000,
      defaultCost: 1,
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
      introspectionCost: 1000, // Make introspection expensive
      createError: (max, actual) => {
        return new Error(
          `Query complexity ${actual} exceeds maximum allowed complexity ${max}`
        );
      }
    })
  ],
  plugins: [
    {
      requestDidStart() {
        return {
          willSendResponse(requestContext) {
            // Log expensive queries for monitoring
            if (requestContext.request.query?.length > 1000) {
              console.warn('Large GraphQL query detected', {
                query: requestContext.request.query,
                variables: requestContext.request.variables,
                userAgent: requestContext.request.http?.headers?.get('user-agent')
              });
            }
          }
        };
      }
    }
  ]
});

// Apply rate limiting to GraphQL endpoint
app.use('/graphql', graphqlLimiter);
3

Implement Spring Boot Rate Limiting with Bucket4j

Use Bucket4j library to implement token bucket rate limiting in Spring Boot applications. Configure different rate limits for different user tiers and API endpoints with proper error handling and monitoring.

View implementation – JAVA
@RestController
@RequestMapping("/api")
public class RateLimitedController {
    
    private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
    
    @Autowired
    private RedisTemplate<String, Bucket> redisTemplate;
    
    // Create bucket for rate limiting
    private Bucket createNewBucket() {
        return Bucket4j.builder()
            .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
            .addLimit(Bandwidth.classic(20, Refill.intervally(20, Duration.ofSeconds(10))))
            .build();
    }
    
    private Bucket resolveBucket(String key) {
        return cache.computeIfAbsent(key, k -> {
            // Try to get from Redis for distributed rate limiting
            Bucket bucket = redisTemplate.opsForValue().get(k);
            if (bucket == null) {
                bucket = createNewBucket();
                redisTemplate.opsForValue().set(k, bucket, Duration.ofHours(1));
            }
            return bucket;
        });
    }
    
    @PostMapping("/expensive-operation")
    public ResponseEntity<?> expensiveOperation(
        HttpServletRequest request, 
        @RequestBody RequestDto requestDto) {
        
        String key = getClientId(request); // IP or user ID
        Bucket bucket = resolveBucket(key);
        
        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
        
        if (probe.isConsumed()) {
            // Process the request
            try {
                Object result = performExpensiveOperation(requestDto);
                return ResponseEntity.ok(result);
            } catch (Exception e) {
                // Log error and return proper response
                log.error("Error processing request", e);
                return ResponseEntity.status(500).body("Internal server error");
            }
        } else {
            long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
            
            return ResponseEntity.status(429)
                .header("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill))
                .header("X-Rate-Limit-Remaining", String.valueOf(probe.getRemainingTokens()))
                .body(Map.of(
                    "error", "Rate limit exceeded",
                    "retryAfter", waitForRefill + " seconds"
                ));
        }
    }
    
    private String getClientId(HttpServletRequest request) {
        // Prefer authenticated user ID over IP
        String userId = extractUserIdFromToken(request);
        return userId != null ? "user:" + userId : "ip:" + getClientIpAddress(request);
    }
}
4

Configure API Gateway Rate Limiting and Throttling

Properly configure rate limiting at the API gateway level using tools like Kong, AWS API Gateway, or Nginx. Implement tiered rate limiting based on authentication status, user plans, and endpoint criticality.

View implementation – YAML
# Kong API Gateway with rate limiting plugin
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: rate-limiting-plugin
plugin: rate-limiting
config:
  minute: 100
  hour: 1000
  day: 10000
  limit_by: consumer
  policy: redis
  redis_host: redis.default.svc.cluster.local
  redis_port: 6379
  redis_database: 0
  fault_tolerant: true
  
---
# Different rate limits for authenticated vs anonymous users
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: anonymous-rate-limiting
plugin: rate-limiting
config:
  minute: 10
  hour: 100
  limit_by: ip
  policy: redis
  redis_host: redis.default.svc.cluster.local
  
---
# AWS API Gateway with usage plans
Resources:
  UsagePlan:
    Type: AWS::ApiGateway::UsagePlan
    Properties:
      UsagePlanName: StandardPlan
      Description: Standard rate limiting for authenticated users
      Throttle:
        RateLimit: 100
        BurstLimit: 200
      Quota:
        Limit: 10000
        Period: DAY
      ApiStages:
        - ApiId: !Ref MyApi
          Stage: prod
          
  BasicUsagePlan:
    Type: AWS::ApiGateway::UsagePlan
    Properties:
      UsagePlanName: BasicPlan
      Description: Limited rate for anonymous users
      Throttle:
        RateLimit: 10
        BurstLimit: 20
      Quota:
        Limit: 1000
        Period: DAY

Detect This Vulnerability in Your Code

Sourcery automatically identifies missing rate limiting in apis enabling denial of service attacks and many other security issues in your codebase.