API Rate Limiting Bypass Techniques & Defenses: IP Rotation, Header Manipulation, Timing Attacks & Distributed Detection
API Rate Limiting Bypass Techniques & Defenses: IP Rotation, Header Manipulation, Timing Attacks & Distributed Detection
Deep dive into rate limiting bypass techniques used by attackers and the defensive architectures to detect and prevent them. Covers IP-based evasion, header manipulation, timing attacks, and distributed detection systems.
Rate Limiting Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ Rate Limiting Layers │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Layer 1: Edge/CDN │
│ ───────────────── │
│ • IP-based limits │
│ • Geographic blocking │
│ • DDoS protection │
│ • Connection limits │
└───────────────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────────────▼─────────────────────────────────┐
│ Layer 2: API Gateway │
│ ──────────────────── │
│ • API key limits │
│ • User/account limits │
│ • Endpoint-specific limits │
│ • Request complexity limits │
└───────────────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────────────▼─────────────────────────────────┐
│ Layer 3: Application │
│ ──────────────────── │
│ • Business logic limits │
│ • Resource-specific limits │
│ • Operation-specific limits │
│ • Adaptive/dynamic limits │
└─────────────────────────────────────────────────────────────────────┘
Bypass Strategy: Find the weakest layer
Common Bypass Techniques
1. IP Rotation
┌─────────────────────────────────────────────────────────────────────────┐
│ IP Rotation Bypass │
└─────────────────────────────────────────────────────────────────────────┘
Attack Pattern:
───────────────
Rate limit: 100 requests/minute per IP
Attacker with 100 IPs:
• IP-1: 100 requests → rate limited
• IP-2: 100 requests → rate limited
• ...
• IP-100: 100 requests → rate limited
• Total: 10,000 requests/minute
IP Sources:
───────────
• Residential proxy networks (hard to block)
• Cloud provider IP ranges
• Compromised IoT devices (botnets)
• Mobile carrier networks (CGNAT)
• Tor exit nodes
Defense: Multi-Factor Identification
// Beyond IP: Multi-signal rate limiting
class MultiFactorRateLimiter {
private redis: Redis;
private readonly WINDOW_SIZE = 60; // seconds
async checkRateLimit(request: Request): Promise<RateLimitResult> {
// Extract multiple identifiers
const identifiers = this.extractIdentifiers(request);
// Check limits for each identifier type
const results = await Promise.all([
this.checkLimit('ip', identifiers.ip, 100),
this.checkLimit('user', identifiers.userId, 1000),
this.checkLimit('apikey', identifiers.apiKey, 5000),
this.checkLimit('fingerprint', identifiers.fingerprint, 200),
this.checkLimit('asn', identifiers.asn, 10000), // ISP/cloud provider
]);
// Aggregate results
const blocked = results.some(r => r.blocked);
const lowestRemaining = Math.min(...results.map(r => r.remaining));
// Additional: Check for distributed attack patterns
const distributedAttack = await this.detectDistributedAttack(identifiers);
return {
blocked: blocked || distributedAttack,
remaining: lowestRemaining,
retryAfter: blocked ? this.WINDOW_SIZE : 0,
};
}
private extractIdentifiers(request: Request): RequestIdentifiers {
const headers = request.headers;
return {
ip: this.getClientIP(request),
userId: this.extractUserId(request),
apiKey: headers.get('X-API-Key'),
fingerprint: this.generateFingerprint(request),
asn: this.lookupASN(this.getClientIP(request)),
sessionId: this.extractSessionId(request),
};
}
private generateFingerprint(request: Request): string {
// Combine multiple signals for fingerprinting
const signals = [
request.headers.get('User-Agent') || '',
request.headers.get('Accept-Language') || '',
request.headers.get('Accept-Encoding') || '',
request.headers.get('Accept') || '',
// TLS fingerprint (JA3) if available
(request as any).tlsFingerprint || '',
];
return this.hash(signals.join('|'));
}
// Detect distributed attack: Many IPs, same pattern
private async detectDistributedAttack(
identifiers: RequestIdentifiers
): Promise<boolean> {
// Track unique IPs per fingerprint
const key = `distributed:${identifiers.fingerprint}`;
await this.redis.sadd(key, identifiers.ip);
await this.redis.expire(key, this.WINDOW_SIZE);
const uniqueIPs = await this.redis.scard(key);
// If same fingerprint from many IPs, likely distributed attack
if (uniqueIPs > 10) {
console.warn('Distributed attack detected', {
fingerprint: identifiers.fingerprint,
uniqueIPs,
});
return true;
}
return false;
}
}
2. Header Manipulation
┌─────────────────────────────────────────────────────────────────────────┐
│ Header Manipulation Bypass │
└─────────────────────────────────────────────────────────────────────────┘
Spoofed Headers Attackers Try:
──────────────────────────────
X-Forwarded-For: 1.2.3.4 ← Fake origin IP
X-Real-IP: 1.2.3.4 ← Another fake IP header
X-Originating-IP: 1.2.3.4 ← Less common, sometimes trusted
True-Client-IP: 1.2.3.4 ← Cloudflare header
CF-Connecting-IP: 1.2.3.4 ← Another Cloudflare header
X-Client-IP: 1.2.3.4 ← Generic client IP
Why It Works:
─────────────
Many applications blindly trust these headers
without verifying they came from a trusted proxy.
Attack:
POST /api/data
X-Forwarded-For: 1.1.1.1 ← Fake IP #1
...
POST /api/data
X-Forwarded-For: 2.2.2.2 ← Fake IP #2
Defense: Secure IP Extraction
// Secure client IP extraction
class SecureIPExtractor {
private readonly TRUSTED_PROXIES: Set<string>;
private readonly PRIVATE_RANGES = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^127\./,
/^::1$/,
/^fc00:/,
/^fe80:/,
];
constructor(trustedProxies: string[]) {
this.TRUSTED_PROXIES = new Set(trustedProxies);
}
getClientIP(request: Request, socketIP: string): string {
// Only trust forwarded headers if request came from trusted proxy
if (!this.isTrustedProxy(socketIP)) {
// Request not from trusted proxy - use socket IP
return socketIP;
}
// Parse X-Forwarded-For
const xff = request.headers.get('X-Forwarded-For');
if (xff) {
return this.parseXFF(xff, socketIP);
}
// Fallback to socket IP
return socketIP;
}
private parseXFF(xff: string, socketIP: string): string {
// X-Forwarded-For: client, proxy1, proxy2
// Rightmost is added by closest proxy
const ips = xff.split(',').map(ip => ip.trim());
// Walk from right to left, stopping at first untrusted IP
// That's the real client IP
for (let i = ips.length - 1; i >= 0; i--) {
const ip = ips[i];
if (this.isPrivateIP(ip)) {
continue; // Skip private IPs
}
if (!this.isTrustedProxy(ip)) {
return ip; // First untrusted IP is the client
}
}
// All IPs were trusted/private - use socket IP
return socketIP;
}
private isTrustedProxy(ip: string): boolean {
return this.TRUSTED_PROXIES.has(ip);
}
private isPrivateIP(ip: string): boolean {
return this.PRIVATE_RANGES.some(range => range.test(ip));
}
}
// Usage in rate limiter
const ipExtractor = new SecureIPExtractor([
// Only trust your own infrastructure
'10.0.0.0/8', // Internal network
'192.168.1.1', // Specific proxy
// Cloudflare IPs (if using Cloudflare)
'173.245.48.0/20',
'103.21.244.0/22',
// ... other Cloudflare ranges
]);
app.use((req, res, next) => {
// req.socket.remoteAddress is the actual connection IP
req.clientIP = ipExtractor.getClientIP(
req,
req.socket.remoteAddress
);
next();
});
3. Timing Attacks
┌─────────────────────────────────────────────────────────────────────────┐
│ Timing-Based Bypass │
└─────────────────────────────────────────────────────────────────────────┘
Window Boundary Attack:
───────────────────────
Rate limit: 100 requests per 60-second window
Time: 0s────────────30s────────────60s────────────90s
│ │ │ │
Window: │◄────Window 1────►│◄────Window 2────►│
│ │ │ │
Attack: │ 100 reqs│100 reqs │
│ ▲──────▲ │
│ 59s 61s │
│ │
Result: 200 requests in 2 seconds!
Defense: Sliding window algorithm
Defense: Sliding Window Rate Limiting
// Sliding window log algorithm
class SlidingWindowRateLimiter {
private redis: Redis;
async checkLimit(
key: string,
limit: number,
windowMs: number
): Promise<RateLimitResult> {
const now = Date.now();
const windowStart = now - windowMs;
const redisKey = `ratelimit:${key}`;
// Use Redis sorted set with timestamps as scores
const multi = this.redis.multi();
// Remove old entries outside window
multi.zremrangebyscore(redisKey, 0, windowStart);
// Count entries in current window
multi.zcard(redisKey);
// Add current request
multi.zadd(redisKey, now, `${now}-${Math.random()}`);
// Set expiry on the key
multi.expire(redisKey, Math.ceil(windowMs / 1000));
const results = await multi.exec();
const count = results[1][1] as number;
if (count >= limit) {
// Get oldest entry to calculate retry-after
const oldest = await this.redis.zrange(redisKey, 0, 0, 'WITHSCORES');
const retryAfter = oldest.length > 1
? Math.ceil((parseInt(oldest[1]) + windowMs - now) / 1000)
: Math.ceil(windowMs / 1000);
return {
blocked: true,
remaining: 0,
retryAfter,
};
}
return {
blocked: false,
remaining: limit - count - 1,
retryAfter: 0,
};
}
}
// Sliding window counter (more memory efficient)
class SlidingWindowCounter {
private redis: Redis;
async checkLimit(
key: string,
limit: number,
windowSecs: number
): Promise<RateLimitResult> {
const now = Math.floor(Date.now() / 1000);
const currentWindow = Math.floor(now / windowSecs);
const previousWindow = currentWindow - 1;
const currentKey = `${key}:${currentWindow}`;
const previousKey = `${key}:${previousWindow}`;
// Get counts for current and previous windows
const [currentCount, previousCount] = await this.redis.mget(
currentKey,
previousKey
);
// Calculate position in current window (0-1)
const windowPosition = (now % windowSecs) / windowSecs;
// Weighted count: previous window's portion + current window
const weightedCount =
(parseInt(previousCount || '0') * (1 - windowPosition)) +
parseInt(currentCount || '0');
if (weightedCount >= limit) {
return {
blocked: true,
remaining: 0,
retryAfter: windowSecs - (now % windowSecs),
};
}
// Increment current window
await this.redis.multi()
.incr(currentKey)
.expire(currentKey, windowSecs * 2)
.exec();
return {
blocked: false,
remaining: Math.floor(limit - weightedCount - 1),
retryAfter: 0,
};
}
}
4. Path/Endpoint Manipulation
┌─────────────────────────────────────────────────────────────────────────┐
│ Path Manipulation Bypass │
└─────────────────────────────────────────────────────────────────────────┘
If rate limiting is per-path:
─────────────────────────────
/api/users/123 → Rate limited
/api/users/123/ → Different path? (trailing slash)
/api/users/123? → With empty query
/API/USERS/123 → Case variation
/api/./users/123 → Path traversal
/api/users/../users/123 → Normalized same?
API Version Manipulation:
─────────────────────────
/v1/api/data → Rate limited
/v2/api/data → Same endpoint, different version
/api/v1/data → Different path structure
Defense: Path Normalization
// Normalize paths before rate limiting
class PathNormalizer {
normalize(path: string): string {
// Decode URL encoding
let normalized = decodeURIComponent(path);
// Convert to lowercase
normalized = normalized.toLowerCase();
// Remove trailing slashes
normalized = normalized.replace(/\/+$/, '');
// Remove duplicate slashes
normalized = normalized.replace(/\/+/g, '/');
// Resolve . and ..
const parts = normalized.split('/');
const resolved: string[] = [];
for (const part of parts) {
if (part === '..') {
resolved.pop();
} else if (part !== '.' && part !== '') {
resolved.push(part);
}
}
normalized = '/' + resolved.join('/');
// Remove query string for path-based limiting
normalized = normalized.split('?')[0];
return normalized;
}
// Extract rate limit key from request
getRateLimitKey(request: Request): string {
const url = new URL(request.url);
const normalizedPath = this.normalize(url.pathname);
// Group similar endpoints
// /api/users/123 → /api/users/:id
// /api/posts/456/comments/789 → /api/posts/:id/comments/:id
const genericPath = normalizedPath.replace(
/\/[0-9a-f-]{8,}/gi, // UUIDs and numeric IDs
'/:id'
);
return `${request.method}:${genericPath}`;
}
}
Distributed Attack Detection
// Detect coordinated distributed attacks
class DistributedAttackDetector {
private redis: Redis;
// Track patterns across multiple signals
async analyze(request: Request): Promise<ThreatAssessment> {
const signals = await this.collectSignals(request);
const score = await this.calculateThreatScore(signals);
return {
score,
signals,
action: this.determineAction(score),
};
}
private async collectSignals(request: Request): Promise<AttackSignals> {
const ip = request.clientIP;
const fingerprint = this.getFingerprint(request);
const userAgent = request.headers.get('User-Agent') || '';
const path = new URL(request.url).pathname;
// Parallel signal collection
const [
ipRequestCount,
fingerprintIpCount,
pathRequestCount,
userAgentIpCount,
asnRequestCount,
] = await Promise.all([
// Requests from this IP in last minute
this.getCount(`ip:${ip}`),
// Unique IPs with same fingerprint
this.getUniqueCount(`fp:${fingerprint}`, ip),
// Requests to this path from all sources
this.getCount(`path:${path}`),
// IPs using this exact User-Agent
this.getUniqueCount(`ua:${this.hash(userAgent)}`, ip),
// Requests from same ASN
this.getCount(`asn:${this.getASN(ip)}`),
]);
return {
ipRequestCount,
fingerprintIpCount,
pathRequestCount,
userAgentIpCount,
asnRequestCount,
};
}
private calculateThreatScore(signals: AttackSignals): number {
let score = 0;
// Many IPs with same fingerprint = botnet
if (signals.fingerprintIpCount > 10) {
score += 30 * Math.log10(signals.fingerprintIpCount);
}
// High request rate from single IP
if (signals.ipRequestCount > 60) {
score += 20;
}
// Unusual path request spike
if (signals.pathRequestCount > 1000) {
score += 15;
}
// Many IPs with identical User-Agent
if (signals.userAgentIpCount > 50) {
score += 25;
}
// High ASN concentration
if (signals.asnRequestCount > 5000) {
score += 10;
}
return Math.min(score, 100);
}
private determineAction(score: number): ThreatAction {
if (score >= 80) return 'BLOCK';
if (score >= 60) return 'CHALLENGE'; // CAPTCHA
if (score >= 40) return 'THROTTLE';
if (score >= 20) return 'MONITOR';
return 'ALLOW';
}
private async getCount(key: string): Promise<number> {
const count = await this.redis.get(`count:${key}`);
return parseInt(count || '0');
}
private async getUniqueCount(key: string, member: string): Promise<number> {
await this.redis.sadd(`unique:${key}`, member);
await this.redis.expire(`unique:${key}`, 60);
return this.redis.scard(`unique:${key}`);
}
}
Adaptive Rate Limiting
// Dynamic rate limits based on behavior
class AdaptiveRateLimiter {
private baseLimit = 100;
async getLimit(userId: string, request: Request): Promise<number> {
// Get user's trust score
const trustScore = await this.getUserTrustScore(userId);
// Get current system load
const systemLoad = await this.getSystemLoad();
// Calculate adaptive limit
let limit = this.baseLimit;
// Increase limit for trusted users
if (trustScore > 0.8) {
limit *= 2;
} else if (trustScore < 0.3) {
limit *= 0.5;
}
// Reduce limits during high load
if (systemLoad > 0.8) {
limit *= 0.5;
}
// Paid tier multipliers
const tier = await this.getUserTier(userId);
limit *= this.getTierMultiplier(tier);
return Math.floor(limit);
}
private async getUserTrustScore(userId: string): Promise<number> {
// Factors:
// - Account age
// - Payment history
// - Previous rate limit violations
// - Email verified
// - 2FA enabled
const user = await this.getUser(userId);
let score = 0.5; // Base score
const accountAgeDays = (Date.now() - user.createdAt) / (24 * 60 * 60 * 1000);
score += Math.min(accountAgeDays / 365, 0.2); // Up to 0.2 for 1 year
if (user.emailVerified) score += 0.1;
if (user.mfaEnabled) score += 0.1;
if (user.hadViolations) score -= 0.3;
return Math.max(0, Math.min(1, score));
}
private getTierMultiplier(tier: string): number {
const multipliers: Record<string, number> = {
'free': 1,
'basic': 2,
'pro': 5,
'enterprise': 20,
};
return multipliers[tier] || 1;
}
}
Key Takeaways
-
Multi-Factor Identification: Don't rely solely on IP. Use fingerprinting, API keys, user IDs, and ASN together.
-
Secure IP Extraction: Only trust
X-Forwarded-Forfrom known proxies. Validate the proxy chain. -
Sliding Window: Use sliding window algorithms to prevent window boundary attacks.
-
Path Normalization: Normalize paths before applying rate limits. Handle case, encoding, and traversal.
-
Distributed Detection: Track patterns across multiple IPs. Same fingerprint from many IPs = botnet.
-
Adaptive Limits: Adjust limits based on user trust, system load, and subscription tier.
-
Defense in Depth: Layer rate limiting at CDN, gateway, and application levels.
-
Monitoring: Log all rate limit events for pattern analysis and tuning.
What did you think?