Root cause
SSRF & network boundaries introduced
The webhook sender passes the supplied URL directly to the HTTP client.
How AI contributed: Direct introductionPublished Jun 16, 2026 in
unclecode/crawl4aiAll 2 findings in this repository
Provisional finding. Full causal and release verification remains incomplete.
Root cause
The webhook sender passes the supplied URL directly to the HTTP client.
Fix
The sender validates the webhook URL before opening the HTTP client.
Case facts
@@ -0,0 +53,18 @@+ for attempt in range(self.max_attempts):+ try:+ logger.info(+ f"Sending webhook (attempt {attempt + 1}/{self.max_attempts}) to {webhook_url}"+ )++ response = await client.post(+ webhook_url,+ json=payload,+ headers=merged_headers+ )++ # Success or client error (don't retry client errors)+ if response.status_code < 500:+ if 200 <= response.status_code < 300:+ logger.info(f"Webhook delivered successfully to {webhook_url}")+ return True+ else:@@ -44,13 +44,17 @@ Returns: bool: True if delivered successfully, False otherwise """+ # SECURITY: Validate webhook URL at send time (defense-in-depth against SSRF)+ from utils import validate_webhook_url+ validate_webhook_url(webhook_url)+ default_headers = self.config.get("headers", {}) merged_headers = {**default_headers, **(headers or {})} merged_headers["Content-Type"] = "application/json" - async with httpx.AsyncClient(timeout=self.timeout) as client:+ async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=False) as client: for attempt in range(self.max_attempts): try: logger.info( f"Sending webhook (attempt {attempt + 1}/{self.max_attempts}) to {webhook_url}"AI-assisted change be44cf9d68f2f0875d6b360ceed1949e02af02df6259cce93b2953d1f951598c · Fix ad41e5e9f231dc4ba4bece9504e58ffb6bb79cc27f49980e7b77cc2bceb4254d