GitHub Advisoryadvisory · PyPI

GHSA-8frj-8q3m-xhgm PraisonAI Vulnerable to Server-Side Request Forgery via Unvalidated webhook_url in Jobs API

HIGH Published 2026-04-10 Modified 2026-04-10 Official page ↗
Aliases
CVE-2026-40114
Ecosystem
PyPI
Packages
PraisonAI
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Weaknesses
CWE-918
Reviewed
2026-04-10

Affected packages

• PyPI PraisonAI: < 4.5.128 → fixed in 4.5.128

Description

Summary The `/api/v1/runs` endpoint accepts an arbitrary `webhook_url` in the request body with no URL validation. When a submitted job completes (success or failure), the server makes an HTTP POST request to this URL using `httpx.AsyncClient`. An unauthenticated attacker can use this to make the server send POST requests to arbitrary internal or external destinations, enabling SSRF against cloud metadata services, internal APIs, and other network-adjacent services. Details The vulnerability exists across the full request lifecycle: 1. User input accepted without validation — `models.py:32`: ```python class JobSubmitRequest(BaseModel): webhook_url: Optional[str] = Field(None, description="URL to POST results when complete") ``` The field is a plain `str` with no URL validation — no scheme restriction, no host filtering. 2. Stored directly on the Job object — `router.py:80-86`: ```python job = Job( prompt=body.prompt, ... webhook_url=body.webhook_url, ... ) ``` 3. Used in an outbound HTTP request — `executor.py:385-415`: ```python async def _send_webhook(self, job: Job): if not job.webhook_url: return try: import httpx payload = { "job_id": job.id, "status": job.status.value, "result": job.result if job.status == JobStatus.SUCCEEDED else None, "error": job.error if job.status == JobStatus.FAILED else None, ... } async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( job.webhook_url, # Optional[str]: if v is None: return v parsed = urlparse(v) # Only allow http and https schemes if parsed.scheme not in ("http", "https"): raise ValueError("webhook_url must use http or https scheme") # Block private/internal IP ranges hostname = parsed.hostname if not hostname: raise ValueError("webhook_url must have a valid hostname") try: ip = ipaddress.ip_address(hostname) if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: raise ValueError("webhook_url must not point to private/internal addresses") except ValueError as e: if "must not point" in str(e): raise # hostname is not an IP — resolve and check pass return v ``` Additionally, in `executor.py`, add DNS resolution validation before making the request to prevent DNS rebinding: ```python async def _send_webhook(self, job: Job): if not job.webhook_url: return # Validate resolved IP is not private (prevent DNS rebinding) from urllib.parse import urlparse import socket, ipaddress parsed = urlparse(job.webhook_url) try: resolved_ip = socket.getaddrinfo(parsed.hostname, parsed.port or 443)[0][4][0] ip = ipaddress.ip_address(resolved_ip) if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: logger.warning(f"Webhook blocked for {job.id}: resolved to private IP {resolved_ip}") return except (socket.gaierror, ValueError): logger.warning(f"Webhook blocked for {job.id}: could not resolve {parsed.hostname}") return # ... proceed with httpx.AsyncClient.post() ... ```

Related

Alias · CVE (1)

  • CVE-2026-40114 PraisonAI is a multi-agent teams system. vulnerability

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References