23Network

Bot & DDoS

Bot & DDoS Protection — Instruction 23

Coverage

Bot detection, DDoS mitigation, Slowloris, amplification attacks Rate limiting (advanced), honeypots, connection limits


Rate Limiting (Advanced)

1. Multi-Layer Rate Limiting

// Layer 1: Global rate limit (all requests)
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 200 }))

// Layer 2: Per-endpoint strict limits
app.post('/api/auth/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }))
app.post('/api/auth/register', rateLimit({ windowMs: 60 * 60 * 1000, max: 3 }))
app.post('/api/auth/forgot-password', rateLimit({ windowMs: 60 * 60 * 1000, max: 3 }))
app.post('/api/contact', rateLimit({ windowMs: 60 * 60 * 1000, max: 5 }))

// Layer 3: Per-user rate limit (not just per IP)
// Users behind the same proxy/NAT would share IP limit
app.use('/api', (req, res, next) => {
  const identifier = req.user?.id || req.ip
  rateLimiter.consume(identifier).catch(() => {
    return res.status(429).json({ error: 'Too many requests' })
  })
})

2. Distributed Rate Limiting (Redis-backed)

// 🔴 In-memory rate limiting fails with multiple server instances
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 })
// Each server has its own counter → attacker uses all servers

// 🟢 Redis-backed rate limiting (shared state)
import { RateLimiterRedis } from 'rate-limiter-flexible'
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'middleware',
  points: 100,           // max requests
  duration: 900,         // per 15 minutes
  blockDuration: 60      // block for 1 min after limit hit
})

Request Size & Timeout

3. Request Body Size Limits

// 🔴 Unlimited body size = memory exhaustion
// 🟢 Strict limits by content type
app.use(express.json({ limit: '10kb' }))
app.use(express.urlencoded({ limit: '10kb', extended: false }))

// For file uploads: multer with explicit limits (instruction 14)
// For APIs: 10KB-100KB is usually sufficient

4. Request Timeout

// 🔴 No timeout = request holds server resources forever
// 🟢 Set server and middleware timeouts
import timeout from 'connect-timeout'
app.use(timeout('30s'))
app.use((req, res, next) => {
  if (req.timedout) return res.status(503).json({ error: 'Request timeout' })
  next()
})

// Node.js HTTP server timeout
server.setTimeout(30000)  // 30 seconds
server.keepAliveTimeout = 65000

Slowloris Protection

5. Connection Timeouts (Nginx)

# Slowloris sends partial headers slowly to hold connections open

client_header_timeout 10s;    # time to receive full headers
client_body_timeout 10s;      # time between body reads
send_timeout 10s;             # time between sends to client
keepalive_timeout 65s;        # keep-alive connection limit

6. Connection Limits per IP (Nginx)

limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_conn conn_limit_per_ip 20;    # max 20 concurrent connections per IP

limit_req_zone $binary_remote_addr zone=req_limit:10m rate=30r/m;
limit_req zone=req_limit burst=10 nodelay;

Bot Detection

7. User-Agent Validation

// 🔴 No user-agent check on sensitive endpoints
// 🟢 Basic bot detection
const BOT_PATTERNS = [
  /python-requests/i, /curl/i, /wget/i, /scrapy/i, /mechanize/i,
  /bot/i, /crawler/i, /spider/i, /headless/i
]

app.use('/api/sensitive', (req, res, next) => {
  const ua = req.headers['user-agent'] || ''
  if (!ua || BOT_PATTERNS.some(p => p.test(ua))) {
    // Don't block blindly (legitimate tools use curl/requests)
    // But flag and rate-limit more strictly
    req.isSuspected = true
  }
  next()
})

// Note: User-Agent can be spoofed — use as signal, not sole defense

8. Browser Fingerprint Checks

// Missing typical browser headers = likely bot
app.use((req, res, next) => {
  const suspiciousSignals = []
  
  if (!req.headers['accept-language']) suspiciousSignals.push('no-accept-language')
  if (!req.headers['accept']) suspiciousSignals.push('no-accept')
  if (req.headers['accept'] === '*/*') suspiciousSignals.push('wildcard-accept')
  
  if (suspiciousSignals.length >= 2) {
    req.botScore = (req.botScore || 0) + suspiciousSignals.length
  }
  next()
})

Amplification Attack Prevention

9. Response Size Proportional to Request

// 🔴 Small request → huge response = amplification
// Example: GET /api/users → returns 10,000 users
app.get('/api/users', async (req, res) => {
  const users = await User.findAll()  // returns ALL users!
  res.json(users)
})

// 🟢 Always paginate large collections
app.get('/api/users', async (req, res) => {
  const page = Math.max(1, parseInt(req.query.page) || 1)
  const limit = Math.min(100, parseInt(req.query.limit) || 20)
  const users = await User.findAll({ limit, offset: (page - 1) * limit })
  res.json({ users, page, limit, total: users.count })
})

CAPTCHA Implementation

10. CAPTCHA on Sensitive Forms

// Add CAPTCHA after N failed attempts or for sensitive operations
// Options: hCaptcha (privacy-friendly), Cloudflare Turnstile (free)

// Server-side CAPTCHA verification
app.post('/api/auth/login', async (req, res) => {
  if (req.body.captchaToken) {
    const verified = await verifyCaptcha(req.body.captchaToken)
    if (!verified) return res.status(400).json({ error: 'CAPTCHA failed' })
  }
  // ... login logic
})

// hCaptcha verification
async function verifyCaptcha(token) {
  const response = await fetch('https://hcaptcha.com/siteverify', {
    method: 'POST',
    body: new URLSearchParams({
      secret: process.env.HCAPTCHA_SECRET,
      response: token
    })
  })
  const data = await response.json()
  return data.success
}

DDoS Mitigation (Cloudflare)

11. Cloudflare Configuration Checklist (Guided)

// These require user action in Cloudflare dashboard:
// ☐ Under Attack Mode: enable if active DDoS
// ☐ WAF: enable managed rules (OWASP ruleset)
// ☐ Rate Limiting: configure in Cloudflare dashboard
// ☐ Bot Management: enable (paid plans)
// ☐ Browser Integrity Check: enable
// ☐ Challenge Passage: set to 30 minutes

// In code (Cloudflare Workers):
export default {
  async fetch(request, env) {
    // Cloudflare's CF-IPCountry header for geo-blocking
    const country = request.headers.get('CF-IPCountry')
    if (['CN', 'RU'].includes(country) && isHighRiskEndpoint(request.url)) {
      return new Response('Access restricted', { status: 403 })
    }
    return fetch(request)
  }
}