Skip to content
Back to skills

Rate Limit Bruteforce Shield

ASecurity

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

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 27, 2026
developmentpythongobashnodeexpressgitapi

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill rate-limit-bruteforce-shield --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rate Limit Bruteforce Shield?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Rate Limit Bruteforce Shield
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-rate-limit-bruteforce-shield/badge)](https://www.skillsdirectory.com/skills/rmazrim-rate-limit-bruteforce-shield)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Rate-Limit & Brute-Force Shield
description: Scans sensitive authentication endpoints and implements Redis-backed rate-limiting controls to block credential stuffing and brute-force attacks.
metadata:
  source: skills/rate-limit-bruteforce-shield/rate-limit-bruteforce-shield.md
---

# 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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.