PYSEC-2026-3901

    Dashboard / Vulnerabilities / PYSEC-2026-3901

    PYSEC-2026-3901

    Published: 10 Sept 2026Last Modified: 10 Sept 2026

    Summary: praisonaiagents: ast_grep_rewrite rewrites arbitrary files without the @require_approval gate enforced on every sibling mutation tool

    Details: **Target:** PraisonAI (`MervinPraison/PraisonAI`) **Affected component:** `praisonaiagents/tools/ast_grep_tool.py` — `ast_grep_rewrite` **Affected versions:** master at `ce97667156a116c50b4a3d1aa21e09f048903fda`; reproduced against the current `praisonaiagents` PyPI release (`praisonaiagents` <= 1.6.52). ## Summary Tools in `praisonaiagents/tools/` that modify on-disk state or run code are uniformly wrapped with `@require_approval`, which routes the call through an interactive approval flow before the body runs and fails closed — on denial (or with no approval backend configured) it raises `PermissionError` and the side effect does not occur. This is applied at every sibling mutation entry point: | File | Line | Symbol | Risk level | |---|---|---|---| | `file_tools.py` | 212 | `copy_file` | high | | `file_tools.py` | 239 | `move_file` | high | | `file_tools.py` | 266 | `delete_file` | high | | `edit_tools.py` | 38 | `EditTools.edit_file` | high | | `edit_tools.py` | 155 | `edit_file` | high | | `shell_tools.py` | 32 | `execute_command` | critical | | `python_tools.py` | 352 | `execute_code` | critical | `ast_grep_tool.py:149` `ast_grep_rewrite` is structurally a sibling of these but has no decorator and no `from ..approval import require_approval` import. With `dry_run=False` (LLM-controllable), it builds `sg --pattern <P> --rewrite <R> --lang <L> --update-all <path>` (lines 204–211) and calls `subprocess.run(cmd, ...)` (line 215), modifying every file under `path` matching the pattern. There is no approval gate, no `_validate_path` workspace check, and no `cwd=` sandboxing. The function is registered as a top-level tool (`__init__.py:182`) and exposed via the `code_intelligence` built-in profile (`profiles.py`). A secondary defect: on the `dry_run=False` path `ast_grep_rewrite` returns the literal string `No changes made` to the caller even when it modified files (the "No changes made" return at `ast_grep_tool.py:230` is reached on this path), so an operator inspecting tool output sees no record that a write occurred. ## Proof of concept Single script, clean venv, `praisonaiagents` from PyPI, `ast-grep` CLI installed. `PRAISONAI_AUTO_APPROVE` is removed from the environment first, so no env-bypass is in play. ```python import os, tempfile, textwrap os.environ.pop("PRAISONAI_AUTO_APPROVE", None) workdir = tempfile.mkdtemp(prefix="poc-") target = os.path.join(workdir, "target.py") open(target, "w").write(textwrap.dedent(""" def safe_function(x): return x + 1 def hello(name): return 'hi ' + name """)) # Positive: undecorated tool rewrites the file. from praisonaiagents.tools.ast_grep_tool import ast_grep_rewrite ast_grep_rewrite( pattern="def $FN($$$): return $$$", replacement="def $FN($$$): import os; os.environ['POC_CANARY']='1'; return $$$", lang="python", path=workdir, dry_run=False, ) # Negative control: decorated sibling triggers the approval flow. from praisonaiagents.tools.edit_tools import edit_file edit_file(file_path=target, old_text="def hello(name):", new_text="def hello(name): # X") ``` Result, verified: `ast_grep_rewrite` rewrote `target.py` to contain the injected `import os; os.environ['POC_CANARY']='1'` payload, no approval prompt fired, and the call returned `No changes made`. The subsequent `edit_file` call in the same process rendered the Tool Approval Required panel and, on denial, raised `PermissionError("Execution of edit_file denied: User denied")` without modifying its target. Same process, same approval backend — the only difference is the missing decorator on `ast_grep_rewrite`. ## Threat model An LLM agent running locally whose tool surface includes `ast_grep_rewrite` (via the `code_intelligence` profile or direct import). Triggers: the operator asks the agent to refactor code, or prompt-injection in fetched docs / RAG context / any LLM-visible input steers the agent to call `ast_grep_rewrite` with attacker-chosen `pattern`, `replacement`, and `path` (the `dry_run` field is in the LLM-visible tool schema, so `dry_run=False` is requestable). The agent can then rewrite any file the host process can write — source trees, build configs, dotfiles, the agent's own source. With `path="/"` the rewrite is filesystem-wide. Because the rewrite injects arbitrary text, pointing it at a file that is later imported or executed turns this write primitive into code execution — the basis for the escalation noted in the CVSS line. No operator prompt and no audit record of the modification. ## Suggested fix ```diff --- a/praisonaiagents/tools/ast_grep_tool.py +++ b/praisonaiagents/tools/ast_grep_tool.py @@ from praisonaiagents._logging import get_logger from typing import Optional, List +from ..approval import require_approval @@ +@require_approval(risk_level="high") def ast_grep_rewrite( pattern: str, replacement: str, ``` `high` matches the file-modifying siblings; `critical` is defensible given the write→exec escalation. In the same patch: add a `_validate_path` workspace boundary check (cf. `edit_tools.py:27`); fix the `No changes made` return so it reflects actual modifications; apply the decorator to `ast_grep_scan` (`ast_grep_tool.py:243`) if it can write. `ast_grep_search` is read-only and can stay undecorated. A regression test asserting `ast_grep_rewrite` requires approval (alongside the other mutation tools) would have caught this at review time. ## Coordinated disclosure - Kai Aizen / SnailSploit — `[email protected]` — PGP on request.

    Affected packages

    Package

    Name: praisonaiagents

    Purl: pkg:pypi/praisonaiagents

    Affected ranges

    Type: ECOSYSTEM

    Events:

    Introduced- 0
    Fixed -1.6.58

    Affected versions

    0.0.1
    0.0.10
    0.0.100
    0.0.101
    0.0.102
    0.0.103
    0.0.104
    0.0.105
    0.0.106
    0.0.107
    0.0.108
    0.0.109
    0.0.11
    0.0.110
    0.0.111
    0.0.112
    0.0.113
    0.0.114
    0.0.115
    0.0.116
    0.0.117
    0.0.118
    0.0.119
    0.0.12
    0.0.120
    0.0.121
    0.0.122
    0.0.123
    0.0.124
    0.0.125
    0.0.126
    0.0.127
    0.0.128
    0.0.129
    0.0.13
    0.0.130
    0.0.131
    0.0.132
    0.0.133
    0.0.134
    0.0.135
    0.0.136
    0.0.137
    0.0.138
    0.0.139
    0.0.14
    0.0.140
    0.0.141
    0.0.142
    0.0.143
    0.0.144
    0.0.145
    0.0.146
    0.0.147
    0.0.148
    0.0.149
    0.0.15
    0.0.150
    0.0.151
    0.0.152
    0.0.153
    0.0.154
    0.0.155
    0.0.156
    0.0.157
    0.0.158
    0.0.159
    0.0.16
    0.0.160
    0.0.161
    0.0.162
    0.0.163
    0.0.164
    0.0.165
    0.0.166
    0.0.167
    0.0.168
    0.0.169
    0.0.17
    0.0.170
    0.0.171
    0.0.172
    0.0.173
    0.0.174
    0.0.175
    0.0.176
    0.0.177
    0.0.178
    0.0.179
    0.0.18
    0.0.180
    0.0.181
    0.0.182
    0.0.183
    0.0.184
    0.0.185
    0.0.187
    0.0.188
    0.0.189
    0.0.19
    0.0.190
    0.0.191
    0.0.192
    0.0.193
    0.0.194
    0.0.195
    0.0.196
    0.0.197
    0.0.198
    0.0.199
    0.0.2
    0.0.20
    0.0.21
    0.0.22
    0.0.23
    0.0.24
    0.0.25
    0.0.26
    0.0.27
    0.0.28
    0.0.29
    0.0.3
    0.0.30
    0.0.31
    0.0.32
    0.0.33
    0.0.34
    0.0.35
    0.0.36
    0.0.37
    0.0.38
    0.0.39
    0.0.4
    0.0.40
    0.0.41
    0.0.42
    0.0.43
    0.0.44
    0.0.45
    0.0.46
    0.0.47
    0.0.48
    0.0.49
    0.0.5
    0.0.50
    0.0.51
    0.0.52
    0.0.53
    0.0.54
    0.0.56
    0.0.57
    0.0.58
    0.0.59
    0.0.6
    0.0.60
    0.0.61
    0.0.62
    0.0.63
    0.0.64
    0.0.65
    0.0.66
    0.0.67
    0.0.68
    0.0.69
    0.0.7
    0.0.70
    0.0.71
    0.0.72
    0.0.73
    0.0.74
    0.0.75
    0.0.76
    0.0.77
    0.0.78
    0.0.79
    0.0.8
    0.0.80
    0.0.81
    0.0.82
    0.0.83
    0.0.84
    0.0.85
    0.0.86
    0.0.87
    0.0.88
    0.0.89
    0.0.9
    0.0.90
    0.0.91
    0.0.92
    0.0.93
    0.0.94
    0.0.95
    0.0.96
    0.0.97
    0.0.98
    0.0.99

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High