From 717c9f5d891428e09a6f2ce676a0425992b5ecc5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:45:53 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM]=20?= =?UTF-8?q?Fix=20unbounded=20retries=20on=20client=20errors=20(DoS=20Risk)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: MEDIUM 💡 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. 🎯 Impact: Inefficient API usage, potential triggering of rate limits or account blocks, and delayed error feedback to users. 🔧 Fix: Modified `_retry_request` to check for `httpx.HTTPStatusError` and re-raise immediately if the status code is a client error (400-499) and not 429 (Too Many Requests). ✅ Verification: Verified with a reproduction script that call count reduced from 3 (retries) to 1 (fail fast) for 400 errors. Ran regression tests. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- .jules/sentinel.md | 8 ++++++++ main.py | 11 +++++++++++ 2 files changed, 19 insertions(+) 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)}")