GHSA-4W49-GWV8-FPJG

Vulnerability from github – Published: 2026-10-08 21:57 – Updated: 2026-10-08 21:57
VLAI
Summary
PraisonAI: Jobs API is unauthenticated by default and allows attacker-controlled webhook SSRF
Details

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: praisonai 4.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 set

Issue: 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

  1. Operator runs the Jobs API without PRAISONAI_JOBS_API_KEY (default → unauth).
  2. Attacker POST /api/v1/runs with webhook_url = a rebinding domain.
  3. The validator(s) see a public IP (pass); on completion httpx.post re-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

  1. PRAI-04-01-Jobs-Submit-NoAuth: POST /api/v1/runs {"prompt":"hello"} → 202 (no auth).
  2. 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.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.6.77"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.6.78"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-60091"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-306",
      "CWE-367",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-08T21:57:57Z",
    "nvd_published_at": "2026-07-15T14:18:35Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nPraisonAI\u0027s 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 \u2014 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 \u2192 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` \u2192 `_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 \u2192 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 \u2192 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 \u2192 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 \u2192 DNS-rebinding TOCTOU. (Correction to an earlier static note: the guard exists but is bypassable.)\n\n### Security boundary\nUnauthenticated network peer \u2192 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\"}` \u2192 `202` (no auth).\n2. `PRAI-04-02-Jobs-Webhook-SSRF-Blind`: `POST /api/v1/runs {\"prompt\":\"hello\",\"webhook_url\":\"http://rebind.lab:8081/secret\"}` \u2192 `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.",
  "id": "GHSA-4w49-gwv8-fpjg",
  "modified": "2026-10-08T21:57:57Z",
  "published": "2026-10-08T21:57:57Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4w49-gwv8-fpjg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-60091"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62175"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/praisonai-before-unauthenticated-ssrf-via-webhook-url"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI: Jobs API is unauthenticated by default and allows attacker-controlled webhook SSRF"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…