GHSA-2H4W-29CV-2CMV

Vulnerability from github – Published: 2026-11-10 15:57 – Updated: 2026-11-10 15:57
VLAI
Summary
AG2: Newline injection in SandboxShellTool bypasses allowed=[...] and readonly=True, enabling arbitrary command execution
Details

Summary

SandboxShellTool's restricted modes — allowed=[...] (explicit command allowlist) and readonly=True (implicit READONLY_COMMANDS allowlist) — can both be bypassed by embedding a newline character inside the command string. The filter only inspects a single-line set of shell metacharacters, but execution runs the raw string through sh -c, where a shell treats an embedded newline exactly like a ; statement separator. An agent (or content that steers the agent, e.g. via prompt injection) can therefore run arbitrary shell commands even when the tool was configured to allow only a specific command prefix or only read-only commands. With the default LocalEnvironment() backend this is a real host subprocess (its own docstring: "Commands run via subprocess on the host, so there is no real isolation"), not an isolated container, so this is full host command execution, not merely an escape from an already-sandboxed environment.

Details

Both restricted modes are enforced by ShellAdapter._filter() in ag2/tools/sandbox/adapter/shell.py:

def _filter(self, command: str) -> str | None:
    if self._allowed is not None:
        if not any(matches(p, command) for p in self._allowed):
            return f"Command not allowed: {command!r}"
        if contains_shell_operator(command):
            return f"Command not allowed (shell operators are not permitted in restricted mode): {command!r}"
    ...

readonly=True reuses this exact same branch: SandboxShellTool.__init__ / ShellAdapter.__init__ sets self._allowed = list(READONLY_COMMANDS) when readonly is set and no explicit allowed is given, so both modes share one filter and one bypass.

contains_shell_operator() in ag2/tools/sandbox/filter.py checks the command against:

_SHELL_OPERATORS: tuple[str, ...] = (">", ">>", "|", ";", "&&", "||", "`", "$(")

This tuple has no entry for \n (or \r). matches() only checks that the command starts with an allowed prefix followed by a space or end-of-string; it never inspects the remainder of the string. Execution is:

argv = ["sh", "-c", command]
...
return await sandbox.exec(argv, env=self._env, timeout=self._timeout)

A POSIX shell splits sh -c "<line1>\n<line2>" into two sequential statements, the same as if they were separated by ;. So a command such as "cat allowed_file.txt\nrm -rf /anything":

  1. Passes matches("cat", command) — the string starts with "cat ".
    1. Passes contains_shell_operator(command) — \n is not a recognized operator.
    1. Reaches sh -c "cat allowed_file.txt\nrm -rf /anything", which runs both statements.

Proof of Concept

Verified directly against the current main branch (github.com/ag2ai/ag2, commit a05ff1cb6ff4955ef252a4e0d4a27d3a32ad57f0, cloned 2026-09-21), by importing the real ag2/tools/sandbox/filter.py module (stdlib-only, no other AG2 dependencies needed) and reproducing ShellAdapter._filter()'s allowlist branch verbatim:

=== allowed=['cat'] ===
'cat /etc/passwd; rm -rf /tmp/x'   -> Command not allowed (shell operators are not permitted in restricted mode): ...
'cat /etc/passwd && rm -rf /tmp/x' -> Command not allowed (shell operators are not permitted in restricted mode): ...
'cat /etc/passwd | rm -rf /tmp/x'  -> Command not allowed (shell operators are not permitted in restricted mode): ...
'cat /etc/passwd\nrm -rf /tmp/x'   -> None   <-- BYPASS: newline variant is NOT rejected

=== readonly=True (uses READONLY_COMMANDS as the allowed list) ===
'echo hi\nid'                      -> None   <-- BYPASS in readonly mode too

=== sh -c really treats the embedded newline as a second statement (mirrors ShellAdapter._exec_async) ===
$ sh -c 'echo first-command-output\necho SECOND-STATEMENT-RAN-$((21*2))'
first-command-output
SECOND-STATEMENT-RAN-42

The three standard chaining attempts (;, &&, |) are correctly rejected by the real filter, confirming it does real work; the newline-separated variant, expressing the same intent, is accepted by the identical filter in both restricted modes, and a real sh -c call confirms the shell executes both statements.

Impact

Any agent configured with SandboxShellTool(allowed=[...]) or SandboxShellTool(readonly=True) — the two configurations the tool's own docstring presents as actual security boundaries, as opposed to blocked, which the same docstring explicitly says "is not a security boundary" — can be made to execute arbitrary shell commands via a single embedded newline in the command argument. Because tool arguments are LLM-generated and can be steered by content the agent processes (a fetched document, tool output, or any other text reaching the model — i.e. indirect prompt injection), this is reachable without the operator of the agent writing the malicious command themselves. With the default LocalEnvironment() backend, this is command execution on the host process running the agent, not confined to a container.

Suggested Remediation

  • Add "\n" and "\r" to _SHELL_OPERATORS in ag2/tools/sandbox/filter.py so contains_shell_operator() rejects embedded newlines the same way it rejects ;.
    • More robustly, execute allowlisted/readonly commands without invoking a shell in restricted mode (shlex.split + exec with no sh -c wrapper), so redirection, chaining, substitution, and every kind of statement separator are inert by construction instead of relying on an enumerated denylist.

Environment Tested

  • Repository: github.com/ag2ai/ag2, commit a05ff1cb6ff4955ef252a4e0d4a27d3a32ad57f0 (main), cloned 2026-09-21.
    • PyPI package ag2, version 1.0.5 per pyproject.toml on this commit.
  • - Python 3, stdlib only (subprocess, importlib) against the real, unmodified ag2/tools/sandbox/filter.py — no AG2 runtime dependencies required to reproduce.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "ag2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.13.2"
            },
            {
              "fixed": "1.1.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "autogen"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.13.2"
            },
            {
              "last_affected": "0.14.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-113663"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-184",
      "CWE-78"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-11-10T15:57:17Z",
    "nvd_published_at": "2026-11-05T23:16:50Z",
    "severity": "HIGH"
  },
  "details": "### Summary\nSandboxShellTool\u0027s restricted modes \u2014 allowed=[...] (explicit command allowlist) and readonly=True (implicit READONLY_COMMANDS allowlist) \u2014 can both be bypassed by embedding a newline character inside the command string. The filter only inspects a single-line set of shell metacharacters, but execution runs the raw string through `sh -c`, where a shell treats an embedded newline exactly like a `;` statement separator. An agent (or content that steers the agent, e.g. via prompt injection) can therefore run arbitrary shell commands even when the tool was configured to allow only a specific command prefix or only read-only commands. With the default LocalEnvironment() backend this is a real host subprocess (its own docstring: \"Commands run via subprocess on the host, so there is no real isolation\"), not an isolated container, so this is full host command execution, not merely an escape from an already-sandboxed environment.\n\n### Details\nBoth restricted modes are enforced by `ShellAdapter._filter()` in `ag2/tools/sandbox/adapter/shell.py`:\n\n```python\ndef _filter(self, command: str) -\u003e str | None:\n    if self._allowed is not None:\n        if not any(matches(p, command) for p in self._allowed):\n            return f\"Command not allowed: {command!r}\"\n        if contains_shell_operator(command):\n            return f\"Command not allowed (shell operators are not permitted in restricted mode): {command!r}\"\n    ...\n```\n\n`readonly=True` reuses this exact same branch: `SandboxShellTool.__init__` / `ShellAdapter.__init__` sets `self._allowed = list(READONLY_COMMANDS)` when `readonly` is set and no explicit `allowed` is given, so both modes share one filter and one bypass.\n\n`contains_shell_operator()` in `ag2/tools/sandbox/filter.py` checks the command against:\n\n```python\n_SHELL_OPERATORS: tuple[str, ...] = (\"\u003e\", \"\u003e\u003e\", \"|\", \";\", \"\u0026\u0026\", \"||\", \"`\", \"$(\")\n```\n\nThis tuple has no entry for `\\n` (or `\\r`). `matches()` only checks that the command starts with an allowed prefix followed by a space or end-of-string; it never inspects the remainder of the string. Execution is:\n\n```python\nargv = [\"sh\", \"-c\", command]\n...\nreturn await sandbox.exec(argv, env=self._env, timeout=self._timeout)\n```\n\nA POSIX shell splits `sh -c \"\u003cline1\u003e\\n\u003cline2\u003e\"` into two sequential statements, the same as if they were separated by `;`. So a command such as `\"cat allowed_file.txt\\nrm -rf /anything\"`:\n\n1. Passes `matches(\"cat\", command)` \u2014 the string starts with `\"cat \"`.\n2. 2. Passes `contains_shell_operator(command)` \u2014 `\\n` is not a recognized operator.\n3. 3. Reaches `sh -c \"cat allowed_file.txt\\nrm -rf /anything\"`, which runs both statements.\n### Proof of Concept\nVerified directly against the current `main` branch (github.com/ag2ai/ag2, commit a05ff1cb6ff4955ef252a4e0d4a27d3a32ad57f0, cloned 2026-09-21), by importing the real `ag2/tools/sandbox/filter.py` module (stdlib-only, no other AG2 dependencies needed) and reproducing `ShellAdapter._filter()`\u0027s allowlist branch verbatim:\n\n```\n=== allowed=[\u0027cat\u0027] ===\n\u0027cat /etc/passwd; rm -rf /tmp/x\u0027   -\u003e Command not allowed (shell operators are not permitted in restricted mode): ...\n\u0027cat /etc/passwd \u0026\u0026 rm -rf /tmp/x\u0027 -\u003e Command not allowed (shell operators are not permitted in restricted mode): ...\n\u0027cat /etc/passwd | rm -rf /tmp/x\u0027  -\u003e Command not allowed (shell operators are not permitted in restricted mode): ...\n\u0027cat /etc/passwd\\nrm -rf /tmp/x\u0027   -\u003e None   \u003c-- BYPASS: newline variant is NOT rejected\n\n=== readonly=True (uses READONLY_COMMANDS as the allowed list) ===\n\u0027echo hi\\nid\u0027                      -\u003e None   \u003c-- BYPASS in readonly mode too\n\n=== sh -c really treats the embedded newline as a second statement (mirrors ShellAdapter._exec_async) ===\n$ sh -c \u0027echo first-command-output\\necho SECOND-STATEMENT-RAN-$((21*2))\u0027\nfirst-command-output\nSECOND-STATEMENT-RAN-42\n```\n\nThe three standard chaining attempts (`;`, `\u0026\u0026`, `|`) are correctly rejected by the real filter, confirming it does real work; the newline-separated variant, expressing the same intent, is accepted by the identical filter in both restricted modes, and a real `sh -c` call confirms the shell executes both statements.\n\n### Impact\nAny agent configured with `SandboxShellTool(allowed=[...])` or `SandboxShellTool(readonly=True)` \u2014 the two configurations the tool\u0027s own docstring presents as actual security boundaries, as opposed to `blocked`, which the same docstring explicitly says \"is not a security boundary\" \u2014 can be made to execute arbitrary shell commands via a single embedded newline in the command argument. Because tool arguments are LLM-generated and can be steered by content the agent processes (a fetched document, tool output, or any other text reaching the model \u2014 i.e. indirect prompt injection), this is reachable without the operator of the agent writing the malicious command themselves. With the default LocalEnvironment() backend, this is command execution on the host process running the agent, not confined to a container.\n\n### Suggested Remediation\n- Add \"\\n\" and \"\\r\" to `_SHELL_OPERATORS` in `ag2/tools/sandbox/filter.py` so `contains_shell_operator()` rejects embedded newlines the same way it rejects `;`.\n- - More robustly, execute allowlisted/readonly commands without invoking a shell in restricted mode (`shlex.split` + exec with no `sh -c` wrapper), so redirection, chaining, substitution, and every kind of statement separator are inert by construction instead of relying on an enumerated denylist.\n### Environment Tested\n- Repository: github.com/ag2ai/ag2, commit a05ff1cb6ff4955ef252a4e0d4a27d3a32ad57f0 (main), cloned 2026-09-21.\n- - PyPI package `ag2`, version 1.0.5 per pyproject.toml on this commit.\n- - Python 3, stdlib only (`subprocess`, `importlib`) against the real, unmodified `ag2/tools/sandbox/filter.py` \u2014 no AG2 runtime dependencies required to reproduce.\n-",
  "id": "GHSA-2h4w-29cv-2cmv",
  "modified": "2026-11-10T15:57:17Z",
  "published": "2026-11-10T15:57:17Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/ag2ai/ag2/security/advisories/GHSA-2h4w-29cv-2cmv"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-113663"
    },
    {
      "type": "WEB",
      "url": "https://github.com/ag2ai/ag2/commit/3cd86726bb731eb11d6270f0ef9e50e42b5abb39"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ag2ai/ag2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/ag2ai/ag2/releases/tag/v1.1.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AG2: Newline injection in SandboxShellTool bypasses allowed=[...] and readonly=True, enabling arbitrary command execution"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.