T09 · Insecure Skill Coding Practices
- Location
scripts/cnpj_lookup.py:169- Finding
Unbounded Recursive Retry on HTTP 429 Responses
- Content
View full analysis
Vulnerability Details
File Location:
scripts/cnpj_lookup.py:169-175
Vulnerability Type: Unbounded recursive retry causing denial of service
Risk Level: MediumVulnerable 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 returnNonewhile this cycle continues,fetch_with_fallback()cannot proceed to the next provider.The
Retry-Aftervalue is also converted directly withint(). An HTTP-date or malformed value can raiseValueError; 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
- A user initiates a CNPJ lookup.
- The application sends a request to a configured provider.
- The provider, or an intermediary controlling the response path, repeatedly returns HTTP 429.
- Each response causes
fetch_url()to sleep and invoke itself recursively. - The process remains blocked, fallback providers are not reached, and stack frames accumulate.
- Continued responses eventually cause stack exhaustion and a
RecursionError, ter ...[truncated 587 chars]
- Remediation
View remediation
Remediation Suggestions
Replace recursive retry with a bounded iterative loop:
- Set an explicit maximum retry count, preferably one retry to match the documented behavior.
- After the retry limit is reached, return
Nonesofetch_with_fallback()can try the next provider. - Parse
Retry-Afterdefensively. Support both integer delay values and HTTP-date values, and reject malformed or unreasonable values. - Cap all wait periods to a documented maximum.
- Log retry attempts and final provider failure without exposing sensitive response data.
- Add tests for persistent 429 responses, malformed
Retry-Afterheaders, 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
