When the Sandbox Bites Back: Three Agent CVEs That Rewrote Containment Rules

·12 min read·Evergreen Tools Team

The most useful documents about AI agents in 2026 are not model cards. They are three security advisories. One is against Cursor (CVE-2026-22708, CVSS 9.8 on the v3.1 scale and 7.2 on v4.0, published January 14, 2026), one against OpenAI's Codex CLI (CVE-2025-59532, CVSS v4.0 8.6), and one against Flowise (CVE-2025-59528, CVSS v3.1 10.0). All three are readable in full on the National Vulnerability Database and in the affected projects' GitHub security advisories. They look unrelated. They share one root cause: containment logic that trusted data produced by the model, or auto-approved exactly the commands an attacker needed. Here is what the advisories actually say, and the five controls that close the hole.

An allowlist should never become the attack helper

An allowlist should never become the attack helper

1. The Shared Pattern: Treating Data as Policy

An agent sandbox is a boundary: which paths are writable, which commands may run, which hosts are reachable. That boundary has to come from the operator and be enforced there. In each of these cases, part of the boundary was handed to the model - either a model-generated path was promoted to writable root, or the meaning of the trusted-commands list could be rewritten through the environment. Neither is a prompt-engineering problem. Both are permission-model problems, which is precisely the shift the OWASP GenAI Security Project made in version 2.01 of its State of Agentic AI Security and Governance report: out with plausible threats, in with CVEs, vendor advisories, and breach reports.

# sandbox.py - the boundary must come from the operator, never the model
def writable_root(session_start_cwd: str, model_suggested_cwd: str) -> str:
    root = os.path.realpath(session_start_cwd)
    # Canonicalise and verify; refuse anything outside the operator's root.
    candidate = os.path.realpath(os.path.join(root, model_suggested_cwd))
    if not candidate.startswith(root + os.sep) and candidate != root:
        raise SandboxViolation(f"{candidate} escapes {root}")
    return candidate

# The CVE-2025-59532 lesson in one line: a model-generated cwd is data,
# not policy. The policy root is where the human started the session.
The sandbox boundary comes from the operator, not the model

The sandbox boundary comes from the operator, not the model

2. CVE-2026-22708: The Allowlist That Auto-Approved the Attack

The NVD record is unambiguous. Before version 2.3, when the Cursor Agent ran in Auto-Run mode with Allowlist mode enabled, certain shell built-ins could still execute without appearing in the allowlist and without requiring user approval. That let an attacker, via direct or indirect prompt injection, poison the shell environment by setting, modifying, or removing variables that influence trusted commands - so an auto-approved command like git branch could deliver an arbitrary payload. It is fixed in 2.3. The lesson is narrow and practical: an allowlist is a promise that a command's semantics are fixed. Shell built-ins rewrite the environment that later commands run in, so their semantics are not fixed, and listing them as trusted is self-defeating.

# env_guard.py - shell built-ins can rewrite the environment they run in
SHELL_BUILTINS = {"export", "set", "unset", "alias", "source", "cd", "eval",
                  "readonly", "declare", "typeset", "trap", "umask", "hash"}

def sanitise(command_argv: list[str]) -> list[str]:
    if not command_argv:
        raise PermissionError("empty command")
    head = os.path.basename(command_argv[0])
    if head in SHELL_BUILTINS:
        raise PermissionError(f"shell built-in not allowed: {head}")
    return command_argv

# CVE-2026-22708: an allowlist that auto-approved trusted commands, while
# built-ins quietly poisoned the environment those commands relied on.
The outbound channel is the third leg of the trifecta

The outbound channel is the third leg of the trifecta

3. CVE-2025-59532: The Model Wrote Its Own Boundary

In Codex CLI versions 0.2.0 through 0.38.0, a bug in the sandbox configuration logic let the tool treat a model-generated cwd as the sandbox's writable root, including paths outside the folder where the user started the session. That bypassed the intended workspace boundary and enabled arbitrary file writes and command execution wherever the Codex process had permissions; the network-disabled sandbox restriction was not affected. The fix in 0.39.0 is exactly the principle this article argues for: it canonicalises and validates that the boundary is based on where the user started the session, and not on the value the model generated. What got patched was not the prompt. It was the source of authority.

# allowlist.py - deny by default, then keep the list tiny
ALLOWED = {
    ("git", "status"), ("git", "diff"), ("git", "log"),
    ("pytest",), ("ruff", "check"), ("npm", "test"),
}

def is_allowed(argv: list[str]) -> bool:
    if not argv:
        return False
    for prefix in ALLOWED:
        if tuple(argv[:len(prefix)]) == prefix:
            # No shell, no pipes, no chaining, no redirection.
            return not any(tok in "|&;<>`$()" for tok in argv)
    return False

# An allowlist is a promise that these exact commands are safe.
# The moment it can rewrite its own environment, that promise is void.

4. The Blast Radius Is Bigger Than One Tool

OWASP's State of AI Surveyor tracks 53 agentic projects, 28 of which are coding agents. The five repositories with the most security advisories are all semi-autonomous frameworks or coding agents: n8n with 57, Claude Code with 22, AutoGPT with 15, Dify with 13, and Roo-Code with 11. The same report maps prompt injection to six of the ten categories in the OWASP Top 10 for Agentic Applications. The supply chain is soft too: CVE-2025-59528 is Flowise 3.0.5 executing caller-supplied MCP configuration through the JavaScript Function() constructor, which lands a CVSS 10.0 remote code execution. Protocol layer, agent layer, skill and package layer - different variants of one problem.

# egress.py - the third leg of the lethal trifecta is the network
ALLOWED_HOSTS = {"api.internal.corp", "pypi.org", "registry.npmjs.org"}

def check_egress(url: str):
    host = urlparse(url).hostname or ""
    if host not in ALLOWED_HOSTS:
        raise PermissionError(f"egress blocked: {host}")
    if urlparse(url).scheme not in ("https",):
        raise PermissionError("https only")

# Private data + untrusted content + outbound channel = exfiltration.
# You can keep the first two and still be safe by breaking the third.

5. Two Sentences Worth Memorising

The first is Simon Willison's lethal trifecta: any agent that combines access to private data, exposure to untrusted content, and the ability to communicate externally can be turned into an exfiltration tool by a single injected prompt. The second is Meta's Agents Rule of Two, which treats those three properties as a budget: an agent operating without human approval may satisfy two of the three, and combining all three requires a human in the loop. Anthropic's threat intelligence report, published September 10, 2026, supplies the other end of the evidence: attackers are already using agentic coding across the kill chain, including sub-agents that handle pre- and post-authentication reconnaissance and code review, alongside campaigns that steal AI API keys and reuse the victim's own credentials. Both sides are automating now. Treat this as an operational risk, not a thought experiment.

{
  "event": "agent.tool_call",
  "agent": "claude-code",
  "argv": ["git", "branch", "--show-current"],
  "approved_by": "allowlist:v3",
  "env_diff": {"BASH_ENV": "<injected>"},
  "cwd": "/home/dev/repo",
  "sandbox_root": "/home/dev/repo",
  "decision": "deny",
  "reason": "env_diff modifies a trusted command's runtime",
  "host_reachable": ["api.internal.corp"]
}

// Log the environment diff, not just the command. Both CVEs below were
// invisible in command-only logs.

6. Five Controls, Ordered by Return

First, make the sandbox root come from the operator and canonicalise the candidate path against it, refusing escapes (code 1). Second, refuse shell built-ins and monitor environment differences (code 2). Third, match the allowlist on exact command and subcommand tuples with no pipes, chaining, or redirection (code 3). Fourth, whitelist egress hosts, which removes the third leg of the trifecta outright (code 4). Fifth, make tool-call logs record the environment diff, cwd, and sandbox root rather than the command line alone (code 5) - both CVEs above are invisible to command-only logging. Do those five before spending another hour on prompt-layer guardrails.

📌 Frequently Asked Questions

What exactly did CVE-2026-22708 affect?

In Cursor before version 2.3, certain shell built-ins could execute under Auto-Run with Allowlist mode without appearing in the allowlist or requiring approval, letting prompt injection poison environment variables so auto-approved trusted commands delivered arbitrary payloads. Fixed in 2.3.

What was the root cause of CVE-2025-59532?

In Codex CLI 0.2.0 through 0.38.0, sandbox configuration treated a model-generated cwd as the writable root, bypassing the workspace boundary and enabling arbitrary file writes and command execution. Version 0.39.0 now validates the boundary from the user's session start directory.

Is prompt injection only a coding-agent problem?

No. OWASP maps prompt injection to six of the ten categories in its Top 10 for Agentic Applications, covering browsers, enterprise copilots, and workflow platforms. The risk exists wherever an agent consumes untrusted content and can act.

Why did an allowlist make the attack easier?

Because it coupled auto-approval with an assumption of fixed semantics. When an approved command can rewrite the environment it runs in, the allowlist pre-approves the payload's execution path for the attacker.

If you can only do one thing, what should it be?

Restrict egress. Breaking the outbound channel defeats the lethal trifecta at the lowest cost, then add sandbox-root validation and command policy afterwards.