PraisonAI: Jobs API is unauthenticated by default and allows attacker-controlled webhook SSRF
What fixes it
- 4.6.78praisonai, pipGitHub
- 4.6.78PraisonAICVE List
- 4.6.78praisonai, PyPIOSV
pip install "praisonai>=4.6.78"
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
PraisonAI's Async Jobs API enables its API-key middleware **only when PRAISONAI_JOBS_API_KEY is set**, so by default every endpoint is unauthenticated. An unauthenticated POST /api/v1/runs accepts an attacker-controlled webhook_url; on job completion the server POSTs the job payload to it via httpx. The webhook_url has an SSRF validator (gethostbyname + private-IP check), but it validates at request time while httpx re-resolves at connection time — a DNS-rebinding TOCTOU that reaches internal services. Runtime-confirmed as an unauthenticated, blind SSRF (the internal canary received the POST; the internal response is not returned to the attacker). Severity Medium.
Details
Affected component
- Package:
praisonai4.6.63. Files:src/praisonai/praisonai/jobs/server.py,jobs/router.py,jobs/executor.py,jobs/models.py.
Vulnerable code / root cause
Path: src/praisonai/praisonai/jobs/server.py
Function: create_app
Snippet:
jobs_api_key = os.environ.get("PRAISONAI_JOBS_API_KEY")
# ...
if jobs_api_key:
app.add_middleware(JobsAPIKeyMiddleware) # auth ONLY when env var is setIssue: with the env var unset (default), no auth middleware is added → all endpoints unauthenticated. Default bind is 127.0.0.1.
Path: src/praisonai/praisonai/jobs/router.py
Function: submit_job
Snippet:
@router.post("", response_model=JobSubmitResponse, status_code=202)
async def submit_job(request, response, body: JobSubmitRequest, ...):
# no auth dependency; body.webhook_url is attacker-controlled
job = Job(prompt=body.prompt, webhook_url=body.webhook_url, ...)
await executor.submit(job)Issue: attacker-controlled webhook_url flows into the job with no authentication on the endpoint.
Path: src/praisonai/praisonai/jobs/models.py (validator) and src/praisonai/praisonai/jobs/executor.py (sink)
Function: validate_webhook_url → _send_webhook
Snippet:
# models.py validate_webhook_url (CHECK time)
ip = socket.gethostbyname(hostname)
if ipaddress.ip_address(ip).is_private or ...:
raise ValueError("Webhook URL resolves to a private or restricted network address")
# executor.py _send_webhook (CONNECT time, re-resolves, no pinning)
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(job.webhook_url, json=payload, ...)Issue: the validator resolves the hostname at validation time but does not pin the IP for the httpx.post connection → DNS rebinding (independent resolution at check vs connect) bypasses it and reaches internal services.
Attack flow
- Operator runs the Jobs API without
PRAISONAI_JOBS_API_KEY(default → unauth). - Attacker
POST /api/v1/runswithwebhook_url= a rebinding domain. - The validator(s) see a public IP (pass); on completion
httpx.postre-resolves to an internal IP and connects → SSRF to internal.
Why existing protection is bypassed
Auth is opt-in (only when the env var is set). The webhook_url validator resolves at check time but does not pin the IP for the connection → DNS-rebinding TOCTOU. (Correction to an earlier static note: the guard exists but is bypassable.)
Security boundary
Unauthenticated network peer → server-side request to internal services (Scope: Changed). Default bind 127.0.0.1 limits remote reach unless the operator binds non-loopback.
Proof of Concept
Environment
Real Jobs API (python -m praisonai.jobs.server, no API key) in a local runtime (127.0.0.1:18085), resolver pointed at the controlled rebinding DNS, internal canary Docker-internal only. Runnable assets: PraisonAI-Runtime-Repro\runtime-files\ (docker-compose.jobs.yml).
Steps to reproduce
PRAI-04-01-Jobs-Submit-NoAuth:POST /api/v1/runs {"prompt":"hello"}→202(no auth).PRAI-04-02-Jobs-Webhook-SSRF-Blind:POST /api/v1/runs {"prompt":"hello","webhook_url":"http://rebind.lab:8081/secret"}→202.
Expected result
Unauthenticated job submission should be rejected; the webhook SSRF guard should prevent reaching internal services regardless of DNS timing.
Impact
Unauthenticated job submission (LLM cost abuse); SSRF to internal services (blind, DNS-rebinding); exfiltration of the job result to an attacker-controlled webhook.
Suggested remediation
- Authenticate the Jobs API by default (auto-generate a token / fail closed when binding non-loopback), like the gateway.
- For
webhook_url: resolve once, reject private/loopback/CGNAT/metadata, then pin and connect to the validated IP; disable redirects.
Text from GitHub Advisory Database
Affected versions 54
- praisonai, pip<= 4.6.77GitHub
- PraisonAIaffected: all versions before 4.6.78CVE List
- PraisonAIunaffected: 4.6.78CVE List
- praisonai, PyPIfrom first release, fixed in 4.6.78OSV
- praisonai, PyPI0.0.1OSV
- praisonai, PyPI0.0.10OSV
- praisonai, PyPI0.0.11OSV
- praisonai, PyPI0.0.12OSV
- praisonai, PyPI0.0.13OSV
- praisonai, PyPI0.0.14OSV
- praisonai, PyPI0.0.15OSV
- praisonai, PyPI0.0.16OSV
- praisonai, PyPI0.0.17OSV
- praisonai, PyPI0.0.18OSV
- praisonai, PyPI0.0.19OSV
- praisonai, PyPI0.0.2OSV
- praisonai, PyPI0.0.20OSV
- praisonai, PyPI0.0.21OSV
- praisonai, PyPI0.0.22OSV
- praisonai, PyPI0.0.23OSV
- praisonai, PyPI0.0.24OSV
- praisonai, PyPI0.0.25OSV
- praisonai, PyPI0.0.26OSV
- praisonai, PyPI0.0.27OSV
- praisonai, PyPI0.0.28OSV
- praisonai, PyPI0.0.29OSV
- praisonai, PyPI0.0.3OSV
- praisonai, PyPI0.0.30OSV
- praisonai, PyPI0.0.31OSV
- praisonai, PyPI0.0.32OSV
- praisonai, PyPI0.0.33OSV
- praisonai, PyPI0.0.34OSV
- praisonai, PyPI0.0.35OSV
- praisonai, PyPI0.0.36OSV
- praisonai, PyPI0.0.37OSV
- praisonai, PyPI0.0.38OSV
- praisonai, PyPI0.0.39OSV
- praisonai, PyPI0.0.4OSV
- praisonai, PyPI0.0.40OSV
- praisonai, PyPI0.0.41OSV
- praisonai, PyPI0.0.42OSV
- praisonai, PyPI0.0.43OSV
- praisonai, PyPI0.0.44OSV
- praisonai, PyPI0.0.45OSV
- praisonai, PyPI0.0.46OSV
- praisonai, PyPI0.0.47OSV
- praisonai, PyPI0.0.48OSV
- praisonai, PyPI0.0.49OSV
- praisonai, PyPI0.0.5OSV
- praisonai, PyPI0.0.50OSV
- praisonai, PyPI0.0.52OSV
- praisonai, PyPI0.0.53OSV
- praisonai, PyPI0.0.54OSV
- praisonai, PyPI0.0.55OSV
References 5
- github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4w49-gwv8-fpjgAdvisoryvia CVE List
- vulncheck.com/advisories/praisonai-before-unauthenticated-ssrf-via-webhook-urlAdvisoryvia CVE List
- github.com/advisories/GHSA-4w49-gwv8-fpjgAdvisoryvia GitHub
- nvd.nist.gov/vuln/detail/CVE-2026-60091Referencevia GitHub
- nvd.nist.gov/vuln/detail/CVE-2026-62175Referencevia GitHub