{"schema":"threatline.record/2","record":{"id":"CVE-2026-60091","aliases":["GHSA-4w49-gwv8-fpjg","PYSEC-2026-4249"],"summary":"PraisonAI: Jobs API is unauthenticated by default and allows attacker-controlled webhook SSRF","details":"## Summary\n\nPraisonAI'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.\n\n## Details\n\n### Affected component\n- Package: `praisonai` 4.6.63. Files: `src/praisonai/praisonai/jobs/server.py`, `jobs/router.py`, `jobs/executor.py`, `jobs/models.py`.\n\n### Vulnerable code / root cause\n\nPath:\n`src/praisonai/praisonai/jobs/server.py`\n\nFunction:\n`create_app`\n\nSnippet:\n```python\njobs_api_key = os.environ.get(\"PRAISONAI_JOBS_API_KEY\")\n# ...\nif jobs_api_key:\n    app.add_middleware(JobsAPIKeyMiddleware)   # auth ONLY when env var is set\n```\nIssue: with the env var unset (default), no auth middleware is added → all endpoints unauthenticated. Default bind is `127.0.0.1`.\n\nPath:\n`src/praisonai/praisonai/jobs/router.py`\n\nFunction:\n`submit_job`\n\nSnippet:\n```python\n@router.post(\"\", response_model=JobSubmitResponse, status_code=202)\nasync def submit_job(request, response, body: JobSubmitRequest, ...):\n    # no auth dependency; body.webhook_url is attacker-controlled\n    job = Job(prompt=body.prompt, webhook_url=body.webhook_url, ...)\n    await executor.submit(job)\n```\nIssue: attacker-controlled `webhook_url` flows into the job with no authentication on the endpoint.\n\nPath:\n`src/praisonai/praisonai/jobs/models.py` (validator) and `src/praisonai/praisonai/jobs/executor.py` (sink)\n\nFunction:\n`validate_webhook_url` → `_send_webhook`\n\nSnippet:\n```python\n# models.py validate_webhook_url (CHECK time)\nip = socket.gethostbyname(hostname)\nif ipaddress.ip_address(ip).is_private or ...:\n    raise ValueError(\"Webhook URL resolves to a private or restricted network address\")\n\n# executor.py _send_webhook (CONNECT time, re-resolves, no pinning)\nasync with httpx.AsyncClient(timeout=30.0) as client:\n    response = await client.post(job.webhook_url, json=payload, ...)\n```\nIssue: 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.\n\n### Attack flow\n1. Operator runs the Jobs API without `PRAISONAI_JOBS_API_KEY` (default → unauth).\n2. Attacker `POST /api/v1/runs` with `webhook_url` = a rebinding domain.\n3. The validator(s) see a public IP (pass); on completion `httpx.post` re-resolves to an internal IP and connects → SSRF to internal.\n\n### Why existing protection is bypassed\nAuth 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.)\n\n### Security boundary\nUnauthenticated 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.\n\n## Proof of Concept\n\n### Environment\nReal 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`).\n\n### Steps to reproduce\n1. `PRAI-04-01-Jobs-Submit-NoAuth`: `POST /api/v1/runs {\"prompt\":\"hello\"}` → `202` (no auth).\n2. `PRAI-04-02-Jobs-Webhook-SSRF-Blind`: `POST /api/v1/runs {\"prompt\":\"hello\",\"webhook_url\":\"http://rebind.lab:8081/secret\"}` → `202`.\n\n### Expected result\nUnauthenticated job submission should be rejected; the webhook SSRF guard should prevent reaching internal services regardless of DNS timing.\n\n## Impact\nUnauthenticated job submission (LLM cost abuse); SSRF to internal services (blind, DNS-rebinding); exfiltration of the job result to an attacker-controlled webhook.\n\n## Suggested remediation\n- Authenticate the Jobs API by default (auto-generate a token / fail closed when binding non-loopback), like the gateway.\n- For `webhook_url`: resolve once, reject private/loopback/CGNAT/metadata, then pin and connect to the validated IP; disable redirects.","categories":["Libraries & packages"],"product":"praisonai","vendor":"MervinPraison","ecosystem":"pip","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-4w49-gwv8-fpjg","source_modified_at":"2026-10-08T21:57:57Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"praisonai"},"range":"<= 4.6.77"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-60091","source_modified_at":"2026-07-10T17:00:48.794Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"PraisonAI","vendor":"MervinPraison","versions":[{"version":"0","status":"affected","versionType":"semver","lessThan":"4.6.78"},{"version":"4.6.78","status":"unaffected","versionType":"semver"}],"defaultStatus":"unaffected"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-4w49-gwv8-fpjg","source_modified_at":"2026-10-15T14:56:06.012919287Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"praisonai","ecosystem":"PyPI","purl":"pkg:pypi/praisonai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.6.78"}]}],"versions":["0.0.1","0.0.10","0.0.11","0.0.12","0.0.13","0.0.14","0.0.15","0.0.16","0.0.17","0.0.18","0.0.19","0.0.2","0.0.20","0.0.21","0.0.22","0.0.23","0.0.24","0.0.25","0.0.26","0.0.27","0.0.28","0.0.29","0.0.3","0.0.30","0.0.31","0.0.32","0.0.33","0.0.34","0.0.35","0.0.36","0.0.37","0.0.38","0.0.39","0.0.4","0.0.40","0.0.41","0.0.42","0.0.43","0.0.44","0.0.45","0.0.46","0.0.47","0.0.48","0.0.49","0.0.5","0.0.50","0.0.52","0.0.53","0.0.54","0.0.55"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-4w49-gwv8-fpjg","source_modified_at":"2026-10-08T21:57:57Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"praisonai"},"version":"4.6.78"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-60091","source_modified_at":"2026-07-10T17:00:48.794Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"PraisonAI","ecosystem":null},"version":"4.6.78","basis":"first unaffected version stated in the CVE record"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-4w49-gwv8-fpjg","source_modified_at":"2026-10-15T14:56:06.012919287Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"praisonai","ecosystem":"PyPI","purl":"pkg:pypi/praisonai"},"version":"4.6.78"}],"cvss":7.2,"severity":"High","kev":{"listed":false},"epss":{"probability":0.00301,"percentile":0.21668,"score_date":"2026-10-23"},"published_at":"2026-07-10T13:58:03.638Z","modified_at":"2026-10-15T14:56:06.012919287Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-4w49-gwv8-fpjg","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-08T21:57:57Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-60091","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-07-10T17:00:48.794Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-4w49-gwv8-fpjg","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-15T14:56:06.012919287Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-60091","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4w49-gwv8-fpjg","tags":["vendor-advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-60091","source":"GitHub Advisory Database"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-62175","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://www.vulncheck.com/advisories/praisonai-before-unauthenticated-ssrf-via-webhook-url","tags":["third-party-advisory"]},{"url":"https://github.com/advisories/GHSA-4w49-gwv8-fpjg","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"yes","technical_impact":"partial","assessed_at":"2026-07-10T16:39:40.340456Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-15T14:56:06.012919287Z","content_hash":"ba27a32583d6b64fa549c157c984ff5dd090f3dcdb8856abbaba2769a4c546cd"}}
