T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/cnpj_lookup.py:169
- Finding
- Unbounded Recursive Retry on HTTP 429 Responses## Vulnerability Details **File Location**: `scripts/cnpj_lookup.py:169-175` **Vulnerability Type**: Unbounded recursive retry causing denial of service **Risk Level**: Medium ### Vulnerable Code ```python except urllib.error.HTTPError as e: if e.code == 429: retry_after = e.headers.get("Retry-After") wait = _rate_limiter.backoff(provider, int(retry_after) if retry_after else None) time.sleep(wait) # Tenta novamente uma vez return fetch_url(url, provider) ``` ### Technical Analysis When an API provider returns HTTP 429, `fetch_url()` waits and then calls itself recursively. No retry counter, recursion limit, or terminal condition restricts this behavior. Although the comment states that the request is retried once, the implementation retries indefinitely as long as the provider continues returning 429. Each retry retains another Python stack frame. A provider that persistently returns 429 can therefore keep the process sleeping and recursing until Python raises `RecursionError`. Because the function does not return `None` while this cycle continues, `fetch_with_fallback()` cannot proceed to the next provider. The `Retry-After` value is also converted directly with `int()`. An HTTP-date or malformed value can raise `ValueError`; however, the surrounding provider-level exception handler will generally catch that error and continue fallback, so the confirmed security concern is the unbounded recursive retry. ### Attack Path 1. A user initiates a CNPJ lookup. 2. The application sends a request to a configured provider. 3. The provider, or an intermediary controlling the response path, repeatedly returns HTTP 429. 4. Each response causes `fetch_url()` to sleep and invoke itself recursively. 5. The process remains blocked, fallback providers are not reached, and stack frames accumulate. 6. Continued responses eventually cause stack exhaustion and a `RecursionError`, ter ...[truncated 587 chars]
- Remediation
- ## Remediation Suggestions Replace recursive retry with a bounded iterative loop: 1. Set an explicit maximum retry count, preferably one retry to match the documented behavior. 2. After the retry limit is reached, return `None` so `fetch_with_fallback()` can try the next provider. 3. Parse `Retry-After` defensively. Support both integer delay values and HTTP-date values, and reject malformed or unreasonable values. 4. Cap all wait periods to a documented maximum. 5. Log retry attempts and final provider failure without exposing sensitive response data. 6. Add tests for persistent 429 responses, malformed `Retry-After` headers, and successful fallback after retry exhaustion. Example hardened structure: ```python def fetch_url(url: str, provider: str, max_retries: int = 1) -> Optional[Dict]: for attempt in range(max_retries + 1): try: req = urllib.request.Request( url, headers={"User-Agent": "CNPJ-Lookup/1.0"} ) with urllib.request.urlopen(req, timeout=15) as response: return json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: if e.code != 429: print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr) return None if attempt >= max_retries: return None retry_after = e.headers.get("Retry-After") try: delay = int(retry_after) if retry_after else None except (TypeError, ValueError): delay = None time.sleep(_rate_limiter.backoff(provider, delay)) return None ```
