diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 158f5db..0564216 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -9,3 +9,11 @@ **Prevention:** Explicitly block all known Bidi control characters (U+202A-U+202E, U+2066-U+2069, U+200E-U+200F) in user-visible strings. Also block path separators (/, \) to prevent confusion. **Implementation:** Pre-compiled character sets at module level for performance, tested comprehensively for all 11 blocked Bidi characters. + +## 2026-10-24 - Unbounded Retries on Client Errors (DoS Risk) + +**Vulnerability:** The retry logic blindly retried all `httpx.HTTPError` exceptions, including 400 (Bad Request) and 401/403 (Auth failures). This causes API spamming, potential account lockouts, and delays in error reporting. + +**Learning:** `httpx.HTTPStatusError` (raised by `raise_for_status()`) inherits from `httpx.HTTPError`. Generic `except httpx.HTTPError:` blocks will catch it and retry client errors unless explicitly handled. + +**Prevention:** Inside retry loops, catch `httpx.HTTPStatusError` first. Check `response.status_code`. If `400 <= code < 500` (and not `429`), re-raise immediately. diff --git a/main.py b/main.py index 4b76614..c7a8c27 100644 --- a/main.py +++ b/main.py @@ -600,6 +600,17 @@ def _retry_request(request_func, max_retries=MAX_RETRIES, delay=RETRY_DELAY): response.raise_for_status() return response except (httpx.HTTPError, httpx.TimeoutException) as e: + # Security Enhancement: Do not retry client errors (4xx) except 429 (Too Many Requests). + # Retrying 4xx errors is inefficient and can trigger security alerts or rate limits. + if isinstance(e, httpx.HTTPStatusError): + code = e.response.status_code + if 400 <= code < 500 and code != 429: + if hasattr(e, "response") and e.response is not None: + log.debug( + f"Response content: {sanitize_for_log(e.response.text)}" + ) + raise + if attempt == max_retries - 1: if hasattr(e, "response") and e.response is not None: log.debug(f"Response content: {sanitize_for_log(e.response.text)}")