GitHub Advisoryadvisory · PyPI
GHSA-8frj-8q3m-xhgm PraisonAI Vulnerable to Server-Side Request Forgery via Unvalidated webhook_url in Jobs API
HIGH
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