Skip to content

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

Merged
utksh1 merged 4 commits into
utksh1:mainfrom
prince-pokharna:feat/scan-rate-limiting
Jun 24, 2026
Merged

feat(security): add Redis-backed sliding window rate limiter for scan execution endpoints#1243
utksh1 merged 4 commits into
utksh1:mainfrom
prince-pokharna:feat/scan-rate-limiting

Conversation

@prince-pokharna

Copy link
Copy Markdown
Contributor

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>

prince-pokharna and others added 4 commits June 23, 2026 23:03
… 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>
@utksh1 utksh1 merged commit d522e9a into utksh1:main Jun 24, 2026
13 of 16 checks passed
@utksh1 utksh1 added the gssoc:approved Admin validation: approved for GSSoC scoring label Jun 24, 2026
utksh1 added a commit that referenced this pull request Jun 25, 2026
, #1234, #1232, #1231, #1230, #1229, #1228, #1222, #1221, #1254, #1252, #1251, #1250, #1242, #1255, #1090, #1097, #1112)

Reverting all PR merges performed during this session to restore repository
to pre-session state. This reverts commits:
- e376276 (#1255)
- 18e5101 (#1222)
- e516dd5 (#1230)
- be400fa (#1235)
- d522e9a (#1243)
- bc0b1cd (#1090)
- de56660 (#1097)
- c2a5a53 (#1112)
- eb30d33 (#1254)
- f2a67f7 (#1250)
- 6579112 (#1251)
- 8751d92 (#1252)
- 3ebc935 (#1221)
- 6f5a936 (#1228)
- da15da4 (#1229)
- b66a5b0 (#1231)
- 775c5c2 (#1232)
- b529ddb (#1234)
- 965b001 (#1241)
- d46e573 (#1242)
- dd09955 (#1240)

All PRs are also being reopened and gssoc labels removed.
@utksh1 utksh1 added level:advanced 55 pts difficulty label for advanced contributor PRs type:security Security work category bonus label type:feature Feature work category bonus label area:backend Backend API, database, or service work gssoc:approved Admin validation: approved for GSSoC scoring and removed gssoc:approved Admin validation: approved for GSSoC scoring labels Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Backend API, database, or service work gssoc:approved Admin validation: approved for GSSoC scoring level:advanced 55 pts difficulty label for advanced contributor PRs type:feature Feature work category bonus label type:security Security work category bonus label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants