feat(security): add Redis-backed sliding window rate limiter for scan execution endpoints#1243
Open
prince-pokharna wants to merge 1 commit into
Open
feat(security): add Redis-backed sliding window rate limiter for scan execution endpoints#1243prince-pokharna wants to merge 1 commit into
prince-pokharna wants to merge 1 commit into
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Scan execution endpoints (
POST /scansand related routes) had no requestthrottling. 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
backend/secuscan/rate_limiter.pyScanRateLimiter— sliding window counter via RedisINCR+EXPIREpipeline. Two tiers: per-minute burst + per-hour sustained. Fails open on Redis errors. Real IP fromX-Forwarded-For.backend/secuscan/config.pyscan_rate_limit,scan_rate_window,scan_burst_limit,scan_burst_windowsettings from env varsbackend/secuscan/main.pyapp.stateat startup. Register custom 429 handler.backend/secuscan/routers/scans.pyDepends(check_scan_rate_limit)to scan-triggering POST routes. Zero changes to execution logic.testing/backend/test_rate_limiter.pyBehavior
HTTP 429withRetry-Afterheader and structured JSONSCAN_RATE_LIMIT=0(useful for local dev)Configuration
Example Response (429)
Testing
./testing/test_python.sh # all 12 new tests pass, full suite greenCI Checklist
backend-lint(ruff) — zero warningsbackend-tests(pytest) — all passingformatting-hygiene— ruff format cleanCloses #<996>