Skip to content

feat(security): add Redis-backed sliding window rate limiter for scan execution endpoints#1243

Open
prince-pokharna wants to merge 1 commit into
utksh1:mainfrom
prince-pokharna:feat/scan-rate-limiting
Open

feat(security): add Redis-backed sliding window rate limiter for scan execution endpoints#1243
prince-pokharna wants to merge 1 commit into
utksh1:mainfrom
prince-pokharna:feat/scan-rate-limiting

Conversation

@prince-pokharna

Copy link
Copy Markdown

Summary

Scan execution endpoints (POST /scans and related routes) had no request
throttling. Unrestricted invocation could spawn unlimited concurrent subprocesses
running nmap, ffuf, nuclei and other external tools — risking resource exhaustion,
host instability, and abuse of scanning capabilities.

This PR implements a two-tier Redis-backed sliding window rate limiter using the
Redis client already declared in pyproject.toml.

What Changed

File Change
backend/secuscan/rate_limiter.py New. ScanRateLimiter — sliding window counter via Redis INCR+EXPIRE pipeline. Two tiers: per-minute burst + per-hour sustained. Fails open on Redis errors. Real IP from X-Forwarded-For.
backend/secuscan/config.py Add scan_rate_limit, scan_rate_window, scan_burst_limit, scan_burst_window settings from env vars
backend/secuscan/main.py Initialize limiter on app.state at startup. Register custom 429 handler.
backend/secuscan/routers/scans.py Apply Depends(check_scan_rate_limit) to scan-triggering POST routes. Zero changes to execution logic.
testing/backend/test_rate_limiter.py New. 12 unit tests covering all paths

Behavior

  • Requests within limit: pass through unchanged, zero overhead
  • Requests exceeding limit: HTTP 429 with Retry-After header and structured JSON
  • Redis unavailable: fails open — scan service stays up, warning logged
  • Rate limiting disabled: set SCAN_RATE_LIMIT=0 (useful for local dev)

Configuration

# .env — all optional, these are the defaults
SCAN_RATE_LIMIT=5              # requests per window per IP (0 = disabled)
SCAN_RATE_WINDOW_SECONDS=60   # per-minute window duration
SCAN_BURST_LIMIT=10           # requests per hour per IP
SCAN_BURST_WINDOW_SECONDS=3600

Example Response (429)

HTTP/1.1 429 Too Many Requests
Retry-After: 47
Content-Type: application/json

{
  "error": {
    "error": "rate_limit_exceeded",
    "message": "Scan rate limit exceeded: maximum 5 requests per 60 seconds.",
    "retry_after": 47
  }
}

Testing

./testing/test_python.sh   # all 12 new tests pass, full suite green

CI Checklist

  • backend-lint (ruff) — zero warnings
  • backend-tests (pytest) — all passing
  • formatting-hygiene — ruff format clean

Closes #<996>

… endpoints

Scan execution endpoints that trigger external tools (nmap, ffuf, nuclei,
etc.) had no request throttling, allowing unlimited concurrent subprocess
creation and potential resource exhaustion.

Changes:
- backend/secuscan/rate_limiter.py: New. ScanRateLimiter class using
  Redis sliding window counter (INCR + EXPIRE pipeline). Two-tier limits:
  per-minute burst protection and per-hour sustained limit. Fails open on
  Redis errors to avoid cascading failures. Reads real client IP from
  X-Forwarded-For for reverse-proxy / Docker deployments.
- backend/secuscan/config.py: Add SCAN_RATE_LIMIT, SCAN_RATE_WINDOW_SECONDS,
  SCAN_BURST_LIMIT, SCAN_BURST_WINDOW_SECONDS env vars with safe defaults.
- backend/secuscan/main.py: Initialize ScanRateLimiter on app.state at
  startup. Register custom 429 exception handler.
- backend/secuscan/routers/scans.py: Apply check_scan_rate_limit as a
  FastAPI Depends() on all scan-triggering POST routes. Zero changes to
  scan execution logic.
- testing/backend/test_rate_limiter.py: New. 12 unit tests covering:
  disabled mode, no-Redis fail-open, per-minute enforcement,
  per-hour enforcement, IP extraction (direct + X-Forwarded-For),
  Redis error fail-open, factory function.

Response: HTTP 429 with Retry-After header and structured JSON error body.
Set SCAN_RATE_LIMIT=0 to disable rate limiting in local dev environments.

Closes #<996>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant