# Rate-Limit & Brute-Force Shield

> Scans sensitive authentication endpoints and implements Redis-backed rate-limiting controls to block credential stuffing and brute-force attacks.

- Skill: `rmazrim/rate-limit-brute-force-shield` (Agent Skill)
- Install (CLI): `npx skillmds@latest add rmazrim/rate-limit-brute-force-shield`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rmazrim/rate-limit-brute-force-shield/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: RMAzrim (https://skillmd.com/u/rmazrim)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/rmazrim/rate-limit-brute-force-shield

---


# Rate-Limit & Brute-Force Shield

## 1. System Architecture & Prerequisites
- Python 3.10+ (stdlib only for the scanner: `re`, `os`, `json`, `argparse`, `pathlib`)
- Generated Node.js middleware requires: `express`, `express-rate-limit`, `rate-limit-redis` (or `ioredis`), `redis`
- Generated test client is a single Node script (stdlib `http`)
- No external Python dependencies

## 2. Input/Output Data Contracts

**Input (CLI args):**
```json
{
  "type": "object",
  "properties": {
    "source": { "type": "string", "description": "Source root containing route definitions" },
    "report_dir": { "type": "string", "description": "Output directory", "default": "./rate-shield-reports" }
  },
  "required": ["source"]
}
```

**Output artifacts:**
- `{report_dir}/rate_limit_report.json` — `endpoints: [{path, protected, store}]`
- `{report_dir}/express_rate_limit.js` — hardened Express middleware (Redis store, per-IP+per-account, lockout)
- `{report_dir}/rate_limit_client_test.js` — burst test client asserting 429 on the 101st request

## 3. Production Reference Implementation

```python
#!/usr/bin/env python3
"""Rate-Limit & Brute-Force Shield — auth-endpoint scanner + hardened Express/Redis middleware generator."""

import re
import os
import json
import argparse
from pathlib import Path

SENSITIVE_ROUTE_PATTERNS = [
    (re.compile(r"""(?:['"`][^'"`]*|\s)(/(?:api/)?(?:login|signin|sign-in|sign_up|signup|auth/token|reset-password|forgot-password|verify-otp|verify_otp|otp|2fa|mfa|activate)(?:/[^'"`]*)?['"`])""", re.IGNORECASE | re.MULTILINE),
    "high-risk auth route (login / password reset / OTP / MFA)"),
    (re.compile(r"""(?:app\.(?:post|get)\(|router\.(?:post|get)\(|@app\.(?:post|get)\(|@router\.(?:post|get)\s*\(\s*["'])""", re.IGNORECASE),
    "route mount (context window applied)"),
]

RATE_LIMIT_PATTERNS = [
    re.compile(r"""(?:rateLimit|rate-limit|express-rate-limit|RateLimiter|limiter|throttle|slowDown|isRateLimited|rate_limit)""", re.IGNORECASE),
    re.compile(r"""(?:tooManyRequests|429|Retry-After|retrySeconds|windowMs|max:\s*\d+)""", re.IGNORECASE),
]

SKIP_DIRS = {"node_modules", ".git", "__pycache__", "venv", ".venv", "dist", "build", ".next"}
SOURCE_EXTS = {".js", ".ts", ".jsx", ".tsx"}


def scan_source(source: Path) -> list:
    """Find auth endpoints and whether they are rate-limited. Returns endpoint records."""
    endpoints = []
    for dirpath, dirnames, filenames in os.walk(source):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fname in filenames:
            if Path(fname).suffix not in SOURCE_EXTS:
                continue
            fpath = Path(dirpath) / fname
            try:
                lines = fpath.read_text(encoding="utf-8", errors="replace").splitlines()
            except OSError:
                continue

            for i, line in enumerate(lines):
                path_match = SENSITIVE_ROUTE_PATTERNS[0][0].search(line)
                if not path_match:
                    continue
                route = path_match.group(1).strip("'\"`")
                window_start = max(0, i - 20)
                window_end = min(len(lines), i + 20)
                context = "\n".join(lines[window_start:window_end])
                has_limit = any(p.search(context) for p in RATE_LIMIT_PATTERNS)
                method_match = re.search(r"\.(get|post|put|patch|delete)\s*\(", line, re.IGNORECASE)
                methods = method_match.group(1).upper() if method_match else "ANY"
                endpoints.append({
                    "file": str(fpath),
                    "line": i + 1,
                    "path": route,
                    "methods": methods,
                    "protected": has_limit,
                    "store": "redis" if re.search(r"redis|ioredis|rate-limit-redis", context, re.IGNORECASE) else
                             ("memory" if has_limit else "none"),
                    "snippet": line.strip()[:180],
                })
    return endpoints


EXPRESS_MIDDLEWARE = r'''// express_rate_limit.js — Redis-backed rate limiting + account lockout for auth routes.
// Generated by rate-limit-bruteforce-shield.
// Requires: npm i express express-rate-limit rate-limit-redis ioredis

const rateLimit = require('express-rate-limit');
const { RedisStore } = require('rate-limit-redis');
const Redis = require('ioredis');

const redisClient = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
redisClient.on('error', (err) => console.error('[redis]', err.message));

// Per-IP + per-account sliding window. Key combines client IP and the account
// identifier so credential stuffing across many IPs toward one account is caught.
const WINDOW_MS = 15 * 60 * 1000;        // 15 minutes
const MAX_PER_IP = 5;                    // 5 attempts per IP per window
const MAX_PER_ACCOUNT = 10;              // 10 attempts per account per window
const LOCKOUT_AFTER = 10;                // successes-independent failure count
const LOCKOUT_MS = 30 * 60 * 1000;       // 30 minutes

const ipLimiter = rateLimit({
  windowMs: WINDOW_MS,
  limit: MAX_PER_IP,
  standardHeaders: 'draft-7',            // RateLimit-* headers
  legacyHeaders: false,
  handler: (req, res) => {
    res.setHeader('Retry-After', Math.ceil(WINDOW_MS / 1000));
    return res.status(429).json({ error: 'Too many requests', retryAfterSeconds: WINDOW_MS / 1000 });
  },
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
    prefix: 'rl:ip:',
  }),
  keyGenerator: (req) => req.ip,
  skip: (req) => req.path.toLowerCase().endsWith('/favicon.ico'),
});

// Sliding-window account limiter keyed by SHA-256(account|route|ip-prefix).
// Prevents one attacker rotating IPs from hammering a single account.
const crypto = require('crypto');

function accountKey(req) {
  const account = (req.body && (req.body.username || req.body.email || req.body.account)) || 'anon';
  const bucket = crypto.createHash('sha256')
    .update(`${account}|${req.path}`)
    .digest('hex')
    .slice(0, 24);
  return bucket;
}

const accountLimiter = rateLimit({
  windowMs: WINDOW_MS,
  limit: MAX_PER_ACCOUNT,
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  handler: (req, res) => {
    const retryAfter = Math.ceil(WINDOW_MS / 1000);
    res.setHeader('Retry-After', String(retryAfter));
    return res.status(429).json({ error: 'Too many attempts for this account', retryAfterSeconds: retryAfter });
  },
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
    prefix: 'rl:acct:',
  }),
  keyGenerator: accountKey,
});

// Login route: per-IP limiter, per-account limiter, then lockout counter.
const LOCK_PREFIX = 'lock:acct:';

async function isLocked(req) {
  const key = LOCK_PREFIX + accountKey(req);
  const ttl = await redisClient.ttl(key);
  return ttl > 0;
}

function lockAccount(req) {
  const key = LOCK_PREFIX + accountKey(req);
  return redisClient.multi()
    .set(key, 'locked')
    .expire(key, LOCKOUT_MS / 1000)
    .exec();
}

async function onLoginSuccess(req) {
  const key = LOCK_PREFIX + accountKey(req);
  await redisClient.del(key);
}

async function onLoginFailure(req) {
  const key = `fail:acct:${accountKey(req)}`;
  const failures = await redisClient.incr(key);
  if (failures === 1) await redisClient.expire(key, LOCKOUT_MS / 1000);
  if (failures >= LOCKOUT_AFTER) {
    await redisClient.multi()
      .set(`${LOCK_PREFIX}${accountKey(req)}`, 'locked')
      .expire(`${LOCK_PREFIX}${accountKey(req)}`, LOCKOUT_MS / 1000)
      .del(key)
      .exec();
  }
  return failures;
}

// Account-lockout middleware: run together with the two limiters on /login.
async function lockoutGuard(req, res, next) {
  try {
    const locked = await isLocked(req);
    if (locked) {
      return res.status(429).json({
        error: 'Account temporarily locked',
        retryAfterSeconds: LOCKOUT_MS / 1000,
      });
    }
    next();
  } catch (err) {
    // Fail open on Redis errors to avoid locking out the whole app — log loudly.
    console.error('[lockout]', err.message);
    next();
  }
}

function wireAuthRateLimits(app, loginPath = '/api/auth/login') {
  app.post(loginPath, lockoutGuard, ipLimiter, accountLimiter, async (req, res) => {
    // Your real login handler goes here. Example with fake credential check:
    const ok = req.body && req.body.password === 'letmein';
    if (ok) {
      await onLoginSuccess(req);
      return res.json({ ok: true });
    }
    const failures = await onLoginFailure(req);
    if (failures >= LOCKOUT_AFTER) {
      return res.status(429).json({ error: 'Account locked', retryAfterSeconds: LOCKOUT_MS / 1000 });
    }
    return res.status(401).json({ error: 'Invalid credentials' });
  });

  ['/api/forgot-password', '/api/reset-password', '/api/verify-otp'].forEach((p) => {
    app.post(p, ipLimiter, accountLimiter, (req, res) => {
      res.json({ ok: true });
    });
  });
}

module.exports = {
  ipLimiter,
  accountLimiter,
  lockoutGuard,
  onLoginSuccess,
  onLoginFailure,
  isLocked,
  lockAccount,
  wireAuthRateLimits,
};

// Usage:
//   app.use(require('express').json());
//   require('./express_rate_limit').wireAuthRateLimits(app);
'''


TEST_CLIENT = r'''// rate_limit_client_test.js — burst test: 100 rapid attempts then assert 429.
// Run: node rate_limit_client_test.js [baseUrl]
const http = require('http');

const BASE = process.argv[2] || 'http://127.0.0.1:3000';
const BURST_COUNT = 100;
const TARGET = '/api/auth/login';

function attempt(i) {
  return new Promise((resolve) => {
    const body = JSON.stringify({ username: 'alice', password: 'wrong', account: 'alice' });
    const req = http.request(BASE + TARGET, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
      },
    }, (res) => {
      res.resume();
      res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, index: i }));
    });
    req.on('error', (err) => resolve({ error: err.message, index: i }));
    req.write(body);
    req.end();
  });
}

(async () => {
  const statuses = {};
  let hit429 = false;
  let retryAfter = null;

  for (let i = 1; i <= BURST_COUNT + 5; i++) {
    const r = await attempt(i);
    statuses[r.status] = (statuses[r.status] || 0) + 1;
    if (r.status === 429) hit429 = true;
    if (r.headers && r.headers['retry-after']) retryAfter = r.headers['retry-after'];
    await new Promise((res) => setTimeout(res, 10));
    if (hit429 && i >= BURST_COUNT + 1) break;
  }

  console.log('Rate-limit shield test result');
  console.log('  responses:', statuses);

  const pass = hit429 && statuses['200'] <= BURST_COUNT;
  console.log(pass ? 'PASS  Got 429 after burst' : 'FAIL  No 429 observed (shield not active?)');
  if (retryAfter) console.log('  Retry-After header:', retryAfter);
  process.exit(pass ? 0 : 1);
})();
'''


def main():
    parser = argparse.ArgumentParser(
        description="Rate-Limit & Brute-Force Shield — auth-endpoint scan + Redis-backed hardening assets.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--source", required=True, help="Source root containing route definitions")
    parser.add_argument("--report-dir", default="./rate-shield-reports", help="Output directory")
    args = parser.parse_args()

    source = Path(args.source).resolve()
    if not source.is_dir():
        print(f"[ERROR] Source directory does not exist: {source}")
        raise SystemExit(1)

    report_dir = Path(args.report_dir).resolve()
    report_dir.mkdir(parents=True, exist_ok=True)

    print(f"[INFO] Scanning auth endpoints in: {source}")
    endpoints = scan_source(source)
    unprotected = [e for e in endpoints if not e["protected"]]

    report = {
        "target": str(source),
        "endpoints": endpoints,
        "summary": {
            "auth_endpoints_found": len(endpoints),
            "unprotected_count": len(unprotected),
            "protected_count": len(endpoints) - len(unprotected),
        },
    }

    mgr_path = report_dir / "express_rate_limit.js"
    mgr_path.write_text(EXPRESS_MIDDLEWARE, encoding="utf-8")
    test_path = report_dir / "rate_limit_client_test.js"
    test_path.write_text(TEST_CLIENT, encoding="utf-8")

    report["generated"] = {
        "express_middleware": str(mgr_path),
        "test_client": str(test_path),
    }

    report_path = report_dir / "rate_limit_report.json"
    report_path.write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")

    print(f"[INFO] Found {len(endpoints)} auth endpoint(s), {len(unprotected)} unprotected")
    for e in endpoints:
        flag = "UNPROTECTED" if not e["protected"] else f"protected ({e['store']})"
        print(f"  [{flag}] {e['methods']} {e['path']}  ({e['file']}:{e['line']})")
    print(f"[INFO] Middleware written: {mgr_path}")
    print(f"[INFO] Test client written: {test_path}")
    print(f"[INFO] Report written:      {report_path}")


if __name__ == "__main__":
    main()
```

## 4. Execution Protocol & Step-by-Step Workflow
1. Scan the auth endpoints in your source tree:
   ```bash
   python rate-limit-bruteforce-shield.md --source ./api --report-dir ./rate-shield-reports
   ```
2. Read `rate_limit_report.json` → identify `UNPROTECTED` endpoints (no limiter detected in the ±20-line context).
3. Install middleware dependencies in the target Express app:
   ```bash
   npm install express-rate-limit rate-limit-redis ioredis
   ```
4. Copy `express_rate_limit.js` into the app and wire it after `app.use(express.json())`:
   ```js
   require('./express_rate_limit').wireAuthRateLimits(app);
   ```
5. Ensure Redis is running (`redis-server`), pointing `REDIS_URL` at it if non-default.
6. Run the burst test client against the live server:
   ```bash
   node rate_limit_client_test.js http://127.0.0.1:3000
   ```
7. Assert PASS (429 on request 101+, Retry-After present); fix wiring if FAIL.
8. Re-scan; all sensitive endpoints must now report `protected` with `store: "redis"`.

## 5. Edge Cases & Error Handling
- Redis failures **fail open** (requests proceed, error logged): prevents a Redis outage from DoS-ing the whole app; acceptable trade-off for auth, monitored loudly.
- `keyGenerator` falls back to `'anon'` when the account identifier is missing, so anonymous bursts are still IP-limited.
- Account keys are SHA-256 hashed before use as Redis keys — avoids user-controlled characters breaking key structure.
- Sliding window uses IP AND per-account counters via two chained limiter instances, defeating single-account multi-IP stuffing.
- `Retry-After` is always emitted (asserted by the test client) so clients can back off gracefully.
- The lockout counter resets on success via `onLoginSuccess`; failures increment via `onLoginFailure` with a TTL equal to the lockout window.
- The scanner deliberately uses a ±20-line context window for limiter detection — a limiter imported from another module far away is a known false-positive source; `confidence` is implied by snippet inspection.
- `skip` favicon check prevents polluting the IP bucket with preflight/favicon traffic.

