API & Web

API Security Best Practices: REST GraphQL Rate Limiting and Authentication

Complete guide to securing APIs with authentication rate limiting input validation and automated scanning

API Security Landscape

APIs (Application Programming Interfaces) have become the primary attack surface for modern applications. As businesses adopt microservices, mobile apps, and third-party integrations, APIs handle more sensitive data and business logic than traditional web interfaces. Gartner predicts that by 2025, API attacks will be the most frequent attack vector causing data breaches for enterprise web applications.

API security differs from traditional web security in critical ways:

  • Direct data exposure: APIs return structured data (JSON, XML) without UI-imposed constraints, allowing attackers to request more data than intended
  • Machine-to-machine: No human in the loop means attacks can scale to millions of requests per second
  • Third-party access: APIs are exposed to partners, mobile apps, and JavaScript clients that developers do not fully control
  • Stateless nature: REST APIs are typically stateless, making traditional session-based security controls less applicable

High-profile API breaches demonstrate the impact: Peloton (2021) exposed 3 million user accounts via API authentication bypass, T-Mobile (2021) suffered a breach affecting 76 million customers through API exploitation, and the Capital One breach (2019) exposed 100 million records via SSRF in a cloud API.

OWASP API Security Top 10

The OWASP API Security Top 10 (2023) identifies the most critical API-specific vulnerabilities. While related to the main OWASP Top 10, this list focuses on vulnerabilities unique to API design and implementation.

API1:2023 - Broken Object Level Authorization

APIs expose endpoints that handle object identifiers (user IDs, resource IDs). Without proper authorization checks, attackers manipulate IDs to access unauthorized resources.

Vulnerable (Node.js/Express):

// VULNERABLE: No authorization check
app.get('/api/users/:userId/profile', (req, res) => {
  const profile = db.users.find(req.params.userId);
  res.json(profile); // Returns ANY user's profile!
});

Secure (Node.js/Express):

// SECURE: Verify ownership before returning data
app.get('/api/users/:userId/profile', authenticateToken, (req, res) => {
  if (req.params.userId !== req.user.id && !req.user.isAdmin) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  const profile = db.users.find(req.params.userId);
  res.json(profile);
});

API2:2023 - Broken Authentication

Weak authentication mechanisms, exposed credentials, missing rate limiting on authentication endpoints.

Vulnerable (Python/Flask):

# VULNERABLE: API key in URL, no rate limiting
@app.route('/api/data')
def get_data():
    api_key = request.args.get('api_key')  # Exposed in logs!
    if api_key == 'hardcoded-key':  # Weak check
        return jsonify(data)
    return 'Unauthorized', 401

Secure (Python/Flask):

# SECURE: API key in header, hashed comparison, rate limiting
from flask_limiter import Limiter
from werkzeug.security import check_password_hash

limiter = Limiter(app, key_func=lambda: request.headers.get('X-API-Key'))

@app.route('/api/data')
@limiter.limit("100 per hour")
def get_data():
    api_key = request.headers.get('X-API-Key')
    if not api_key:
        return jsonify({'error': 'Missing API key'}), 401

    stored_hash = db.api_keys.get_hash(api_key[:8])  # Key prefix lookup
    if not stored_hash or not check_password_hash(stored_hash, api_key):
        return jsonify({'error': 'Invalid API key'}), 401

    return jsonify(data)

API3:2023 - Broken Object Property Level Authorization

APIs return more fields than the user should access, or allow updating fields that should be read-only (mass assignment).

Vulnerable (Java/Spring):

// VULNERABLE: Returns sensitive fields
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).get(); // Returns SSN, password hash, etc!
}

Secure (Java/Spring):

// SECURE: DTO pattern to control exposed fields
@GetMapping("/api/users/{id}")
public UserPublicDTO getUser(@PathVariable Long id, Authentication auth) {
    User user = userRepository.findById(id).orElseThrow();

    // Authorization check
    if (!user.getId().equals(auth.getUserId()) && !auth.isAdmin()) {
        throw new ForbiddenException();
    }

    // Return only public fields
    return new UserPublicDTO(user.getId(), user.getName(), user.getEmail());
}

API4:2023 - Unrestricted Resource Consumption

Missing rate limiting, pagination limits, or max file upload sizes enable resource exhaustion attacks.

Vulnerable (Go):

// VULNERABLE: No pagination limit
func GetUsers(w http.ResponseWriter, r *http.Request) {
    users := db.GetAllUsers() // Could return millions of records!
    json.NewEncoder(w).Encode(users)
}

Secure (Go):

// SECURE: Pagination with max limit and rate limiting
import "github.com/ulule/limiter/v3"

func GetUsers(w http.ResponseWriter, r *http.Request) {
    // Rate limiting
    context := limiter.Get(r.Context(), "global")
    if context.Reached {
        http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
        return
    }

    // Pagination with max limit
    limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
    if limit <= 0 || limit > 100 {
        limit = 20 // Default to 20, max 100
    }

    offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))

    users := db.GetUsers(limit, offset)
    json.NewEncoder(w).Encode(users)
}

API5:2023 - Broken Function Level Authorization

Administrative functions accessible to regular users due to missing authorization checks.

Vulnerable (TypeScript/Express):

// VULNERABLE: Admin route with no role check
app.delete('/api/users/:id', authenticateToken, (req, res) => {
  db.users.delete(req.params.id); // Any authenticated user can delete!
  res.json({ success: true });
});

Secure (TypeScript/Express):

// SECURE: Role-based access control middleware
function requireAdmin(req: Request, res: Response, next: NextFunction) {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Admin access required' });
  }
  next();
}

app.delete('/api/users/:id', authenticateToken, requireAdmin, (req, res) => {
  db.users.delete(req.params.id);
  res.json({ success: true });
});

Authentication and Authorization

API Key Authentication

API keys are the simplest authentication method but require careful implementation.

API Key Best Practices:

  • Transmit in Authorization header, never in URL query parameters
  • Store hashed keys in the database (like passwords)
  • Use key prefixes for quick lookup without revealing full keys
  • Implement key rotation and expiration
  • Rate limit by API key

Secure API Key Implementation (Python):

import secrets
import hashlib

def generate_api_key():
    # Generate: prefix_secret (e.g., sk_live_abc123...)
    prefix = "sk_live"
    secret = secrets.token_urlsafe(32)
    return f"{prefix}_{secret}"

def hash_api_key(api_key):
    return hashlib.sha256(api_key.encode()).hexdigest()

# On key creation:
api_key = generate_api_key()
db.keys.create(prefix=api_key[:10], hash=hash_api_key(api_key))
# Return api_key to user (only shown once!)

# On API request:
provided_key = request.headers.get('Authorization').replace('Bearer ', '')
key_record = db.keys.find_by_prefix(provided_key[:10])
if not key_record or hash_api_key(provided_key) != key_record.hash:
    return 401

OAuth 2.0 for Third-Party Access

For APIs accessed by third-party applications, OAuth 2.0 provides delegated access without sharing credentials.

Secure OAuth Token Validation (Node.js):

import jwt from 'jsonwebtoken';

function validateOAuthToken(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.substring(7);

  try {
    const decoded = jwt.verify(token, process.env.OAUTH_PUBLIC_KEY, {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com'
    });

    // Validate scopes
    const requiredScope = 'read:users';
    if (!decoded.scope.split(' ').includes(requiredScope)) {
      return res.status(403).json({ error: 'Insufficient scope' });
    }

    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Rate Limiting and Throttling

Rate limiting prevents API abuse, brute-force attacks, and resource exhaustion. Implement multiple rate limiting strategies:

Token Bucket Algorithm

Allows bursts up to bucket capacity, refilling tokens at a steady rate.

Implementation (Node.js/Express):

import rateLimit from 'express-rate-limit';

// Global rate limit: 100 requests per 15 minutes
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false
});

app.use('/api/', globalLimiter);

// Strict limit for authentication: 5 attempts per hour
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true // Only count failed attempts
});

app.post('/api/login', authLimiter, loginHandler);

Sliding Window Rate Limiting

Implementation (Python/Flask):

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379"
)

@app.route('/api/data')
@limiter.limit("10 per minute")
def api_data():
    return jsonify(data)

# Different limits for different tiers
@app.route('/api/premium')
@limiter.limit("100 per minute", key_func=lambda: request.headers.get('X-API-Key'))
def premium_api():
    return jsonify(premium_data)

Rate Limiting Headers

Return rate limit information in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640000000

Input Validation and Sanitization

API input validation prevents injection attacks, data corruption, and logic errors. Validate all inputs at the API boundary.

Schema Validation

Vulnerable (No Validation - Node.js):

// VULNERABLE: No input validation
app.post('/api/users', (req, res) => {
  const user = db.users.create(req.body); // Accepts ANY fields!
  res.json(user);
});

Secure (Schema Validation - Node.js/Zod):

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().min(18).max(120)
});

app.post('/api/users', (req, res) => {
  try {
    const validated = userSchema.parse(req.body);
    const user = db.users.create(validated);
    res.json(user);
  } catch (err) {
    res.status(400).json({ error: err.errors });
  }
});

Secure (Pydantic Validation - Python/FastAPI):

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, conint, constr

class UserCreate(BaseModel):
    email: EmailStr
    name: constr(min_length=1, max_length=100)
    age: conint(ge=18, le=120)

@app.post("/api/users")
def create_user(user: UserCreate):
    # Pydantic automatically validates
    db_user = db.users.create(user.dict())
    return db_user

Secure (Bean Validation - Java/Spring):

import javax.validation.Valid;
import javax.validation.constraints.*;

public class UserCreateDTO {
    @Email
    @NotNull
    private String email;

    @NotBlank
    @Size(min = 1, max = 100)
    private String name;

    @Min(18)
    @Max(120)
    private Integer age;
}

@PostMapping("/api/users")
public User createUser(@Valid @RequestBody UserCreateDTO dto) {
    return userRepository.save(new User(dto));
}

GraphQL-Specific Security Concerns

GraphQL introduces unique security challenges beyond REST APIs.

Query Depth Limiting

GraphQL allows nested queries that can cause exponential database load.

Vulnerable (No Depth Limit):

// Attacker query:
query {
  user(id: 1) {
    posts {
      comments {
        author {
          posts {
            comments {
              author { ... } // Infinite nesting!
            }
          }
        }
      }
    }
  }
}

Secure (Depth Limiting - Node.js/Apollo):

import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)] // Max depth: 5 levels
});

Query Complexity Analysis

Implementation (Node.js/GraphQL):

import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log('query cost:', cost),
      formatErrorMessage: (cost) => `Query too complex: ${cost}. Maximum allowed: 1000`
    })
  ]
});

Introspection in Production

Disable GraphQL introspection in production to prevent schema enumeration:

const server = new ApolloServer({
  introspection: process.env.NODE_ENV !== 'production'
});

How CodeSlick Scans API Code

CodeSlick provides specialized API security analysis across JavaScript, TypeScript, Python, Java, and Go:

  • Authentication gaps: Detects API endpoints missing authentication middleware, hardcoded API keys, and weak JWT validation
  • Authorization issues: Identifies missing ownership checks (IDOR), missing role-based access control, and privilege escalation vectors
  • CORS misconfigurations: Flags overly permissive CORS policies (Access-Control-Allow-Origin: *) with credentials enabled
  • Input validation: Detects missing input validation, SQL injection, NoSQL injection, and command injection patterns
  • Rate limiting gaps: Identifies authentication and sensitive endpoints without rate limiting
  • API key exposure: Detects API keys in URLs, logs, and client-side code (38 secret patterns)

All findings include CWE classification, CVSS scoring, OWASP API Top 10 mapping, and framework-specific remediation code for Express.js, Flask, FastAPI, Spring Boot, and Go.

CodeSlick runs on every pull request via the GitHub App, on every commit via the CLI, and on-demand at codeslick.dev/analyze.

Scan your API code for authentication gaps CORS misconfigurations and input validation issues across 5 languages.

Frequently Asked Questions

Related Guides