Summary
RateLimiter.can_execute() in backend/secuscan/ratelimit.py tracks request history keyed by plugin_id alone. Because the key contains no per-client identifier, any single user who exhausts the rate limit for a plugin blocks every other user from running that same plugin for the entire window. This is a global resource starvation vulnerability, not a per-client throttle.
Affected File
backend/secuscan/ratelimit.py — RateLimiter.can_execute()
The history dict is indexed by plugin_id (e.g. "nmap"). When the call count for that key reaches the configured limit, can_execute() returns False for all callers regardless of their identity, until the oldest entry ages out.
Reproduction
- Send 50 rapid
POST /tasks requests for plugin_id = "nmap" from client A (or a single unauthenticated IP).
- Client B submits a legitimate nmap scan.
can_execute("nmap") returns False for client B even though client B has never submitted a single request this window.
- Client B receives a rate-limit error that is caused entirely by client A's activity.
Since there is no authentication on the API (see issue #266), any anonymous caller can trigger this for every plugin simultaneously with a small burst.
Impact
- Denial of service: one attacker permanently occupies the rate-limit bucket for a plugin, making that plugin unavailable to all legitimate users.
- Silent starvation: the error response looks like a normal rate-limit reply, so operators have no signal that the exhaustion is externally caused.
- Scales across all plugins: a loop over all registered
plugin_id values can take down every scan capability at once.
Fix
Key the history by (client_ip, plugin_id) or (user_id, plugin_id) so each client has its own independent quota:
# Before
history = self._history.setdefault(plugin_id, deque())
# After
bucket_key = (client_identifier, plugin_id)
history = self._history.setdefault(bucket_key, deque())
client_identifier should be the authenticated user ID when auth is present, or the remote IP as a fallback.
Environment
Summary
RateLimiter.can_execute()inbackend/secuscan/ratelimit.pytracks request history keyed byplugin_idalone. Because the key contains no per-client identifier, any single user who exhausts the rate limit for a plugin blocks every other user from running that same plugin for the entire window. This is a global resource starvation vulnerability, not a per-client throttle.Affected File
backend/secuscan/ratelimit.py—RateLimiter.can_execute()The history dict is indexed by
plugin_id(e.g."nmap"). When the call count for that key reaches the configured limit,can_execute()returnsFalsefor all callers regardless of their identity, until the oldest entry ages out.Reproduction
POST /tasksrequests forplugin_id = "nmap"from client A (or a single unauthenticated IP).can_execute("nmap")returnsFalsefor client B even though client B has never submitted a single request this window.Since there is no authentication on the API (see issue #266), any anonymous caller can trigger this for every plugin simultaneously with a small burst.
Impact
plugin_idvalues can take down every scan capability at once.Fix
Key the history by
(client_ip, plugin_id)or(user_id, plugin_id)so each client has its own independent quota:client_identifiershould be the authenticated user ID when auth is present, or the remote IP as a fallback.Environment
backend/secuscan/ratelimit.pyRateLimiter.can_execute()concurrent_limiterin executor.py