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).
Class: CWE-862 (Missing Authorization).
CVSS v3.1: AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L = 6.1 (base). Escalates to ~8.x if the rewrite targets an imported/executed file (arbitrary write → code execution); see threat model.
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.
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
--- 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
Delivered privately via GitHub Security Advisory; no public CVE filed, no public posting, no non-private vendor contact.
- Kai Aizen / SnailSploit —
kai@snailsploit.com — PGP on request.
Target: PraisonAI (
MervinPraison/PraisonAI)Affected component:
praisonaiagents/tools/ast_grep_tool.py—ast_grep_rewriteAffected versions: master at
ce97667156a116c50b4a3d1aa21e09f048903fda; reproduced against the currentpraisonaiagentsPyPI release (praisonaiagents<= 1.6.52).Class: CWE-862 (Missing Authorization).
CVSS v3.1:
AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L= 6.1 (base). Escalates to ~8.x if the rewrite targets an imported/executed file (arbitrary write → code execution); see threat model.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 raisesPermissionErrorand the side effect does not occur. This is applied at every sibling mutation entry point:file_tools.pycopy_filefile_tools.pymove_filefile_tools.pydelete_fileedit_tools.pyEditTools.edit_fileedit_tools.pyedit_fileshell_tools.pyexecute_commandpython_tools.pyexecute_codeast_grep_tool.py:149ast_grep_rewriteis structurally a sibling of these but has no decorator and nofrom ..approval import require_approvalimport. Withdry_run=False(LLM-controllable), it buildssg --pattern <P> --rewrite <R> --lang <L> --update-all <path>(lines 204–211) and callssubprocess.run(cmd, ...)(line 215), modifying every file underpathmatching the pattern. There is no approval gate, no_validate_pathworkspace check, and nocwd=sandboxing. The function is registered as a top-level tool (__init__.py:182) and exposed via thecode_intelligencebuilt-in profile (profiles.py).A secondary defect: on the
dry_run=Falsepathast_grep_rewritereturns the literal stringNo changes madeto the caller even when it modified files (the "No changes made" return atast_grep_tool.py:230is reached on this path), so an operator inspecting tool output sees no record that a write occurred.Proof of concept
Single script, clean venv,
praisonaiagentsfrom PyPI,ast-grepCLI installed.PRAISONAI_AUTO_APPROVEis removed from the environment first, so no env-bypass is in play.Result, verified:
ast_grep_rewriterewrotetarget.pyto contain the injectedimport os; os.environ['POC_CANARY']='1'payload, no approval prompt fired, and the call returnedNo changes made. The subsequentedit_filecall in the same process rendered the Tool Approval Required panel and, on denial, raisedPermissionError("Execution of edit_file denied: User denied")without modifying its target. Same process, same approval backend — the only difference is the missing decorator onast_grep_rewrite.Threat model
An LLM agent running locally whose tool surface includes
ast_grep_rewrite(via thecode_intelligenceprofile 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 callast_grep_rewritewith attacker-chosenpattern,replacement, andpath(thedry_runfield is in the LLM-visible tool schema, sodry_run=Falseis requestable). The agent can then rewrite any file the host process can write — source trees, build configs, dotfiles, the agent's own source. Withpath="/"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
highmatches the file-modifying siblings;criticalis defensible given the write→exec escalation. In the same patch: add a_validate_pathworkspace boundary check (cf.edit_tools.py:27); fix theNo changes madereturn so it reflects actual modifications; apply the decorator toast_grep_scan(ast_grep_tool.py:243) if it can write.ast_grep_searchis read-only and can stay undecorated. A regression test assertingast_grep_rewriterequires approval (alongside the other mutation tools) would have caught this at review time.Coordinated disclosure
Delivered privately via GitHub Security Advisory; no public CVE filed, no public posting, no non-private vendor contact.
kai@snailsploit.com— PGP on request.