agentscope-ai / agentscope-ai/agentscope
[Bug]: `tee` is classified read-only, so its write is auto-ALLOWed while the equivalent redirection is not
- 主要言語
- Python
- スター
- 31.6k
- フォーク
- 3.5k
- 平均マージ
- 1日 16時間
- マージ済み PR(30日)
- 103
説明
- [x] I have searched the existing [issues](https://github.com/agentscope-ai/agentscope/issues) and [discussions](https://github.com/agentscope-ai/agentscope/discussions), and this is not a duplicate.
- [x] This is a bug, not a usage question. (For questions, please use [Discussions](https://github.com/agentscope-ai/agentscope/discussions/new?category=general) instead.)
### Background / Description
`tee` writes the file it is given, but `BashCommandParser` classifies it as read-only, so commands that write through `tee` are auto-ALLOWed while the equivalent redirection is refused the read-only label.
`_is_single_command_read_only()` ends with a membership test against `SAFE_COMMANDS`, and `tee` is in that set:
```python
# src/agentscope/tool/_builtin/_bash_parser.py:25-36
SAFE_COMMANDS: Set[str] = {
"echo", "cat", "ls", "pwd", "cd", "true", "false", "printf", "grep",
"tee", # <-- writes files
}
# src/agentscope/tool/_builtin/_bash_parser.py:233-235
# Check if base command is in safe commands
if base_cmd in SAFE_COMMANDS:
return True
```
The same module refuses the read-only label for redirection *precisely because* it writes:
```python
# src/agentscope/tool/_builtin/_bash_parser.py:175-177
# Check for output redirections - these are NOT read-only
if ">" in cmd:
return False
```
So two spellings of the same write get different verdicts. Measured against `main`:
| command | `is_read_only_command()` | `await Bash.check_read_only()` |
| --- | --- | --- |
| `echo x > /tmp/out.txt` | `False` | `False` |
| `echo x \| tee /tmp/out.txt` | **`True`** | **`True`** |
| `echo x >> /tmp/out.txt` | `False` | `False` |
| `echo x \| tee -a /tmp/out.txt` | **`True`** | **`True`** |
| `cat /etc/hosts > /tmp/out.txt` | `False` | `False` |
| `cat /etc/hosts \| tee /tmp/out.txt` | **`True`** | **`True`** |
**Consequence.** `Bash.check_permissions()` auto-ALLOWs whatever `check_read_only()` accepts, in every mode, before the dangerous-command / dangerous-path checks are reached. Verified with a `DENY` rule covering the target path:
```python
deny = PermissionRule(tool_name="Bash", rule_content="Bash(/tmp/**)",
behavior=PermissionBehavior.DENY, source="probe")
ctx = PermissionContext(mode=PermissionMode.DEFAULT, rules=[deny])
echo x > /tmp/out.txt -> PASSTHROUGH (rule is consulted)
echo x | tee /tmp/out.txt -> ALLOW (rule is never consulted)
echo x >> /tmp/out.txt -> PASSTHROUGH
echo x | tee -a /tmp/out.txt -> ALLOW
```
To be precise about severity: `tee` cannot execute arbitrary commands, so this is **not** command execution and not comparable to the separator bypass in #2470. It is a write performed without the rule/decision path that the equivalent redirection goes through, and a deny rule scoped to the target path does not reach it. I am reporting it as an inconsistency in the read-only classifier rather than as RCE.
The module already treats "a read-only-looking command that actually mutates" as a real concern — it has dedicated handling for `find` for exactly this reason:
```python
# src/agentscope/tool/_builtin/_bash_parser.py:214-215
if self._is_mutating_find_command(cmd):
return False
```
`tee` is the same shape of problem and has no equivalent guard.
### Error Messages
No exception is raised — that is the issue. The observable difference is the permission verdict:
```
echo x > /tmp/out.txt -> PermissionBehavior.PASSTHROUGH
echo x | tee /tmp/out.txt -> PermissionBehavior.ALLOW
```
### Steps to Reproduce
```python
import asyncio
from agentscope.permission import (PermissionBehavior, PermissionContext,
PermissionMode, PermissionRule)
from agentscope.tool._builtin._bash import Bash
from agentscope.tool._builtin._bash_parser import BashCommandParser
p = BashCommandParser()
print(p.is_read_only_command("echo x > /tmp/out.txt")) # False
print(p.is_read_only_command("echo x | tee /tmp/out.txt")) # True
async def main():
tool = Bash()
print(await tool.check_read_only({"command": "echo x > /tmp/out.txt"})) # False
print(await tool.check_read_only({"command": "echo x | tee /tmp/out.txt"})) # True
deny = PermissionRule(tool_name="Bash", rule_content="Bash(/tmp/**)",
behavior=PermissionBehavior.DENY, source="probe")
ctx = PermissionContext(mode=PermissionMode.DEFAULT, rules=[deny])
for c in ("echo x > /tmp/out.txt", "echo x | tee /tmp/out.txt"):
d = await tool.check_permissions({"command": c}, ctx)
print(d.behavior, c)
asyncio.run(main())
```
### Proposed fix (happy to open a PR)
Remove `tee` from `SAFE_COMMANDS`. That set is used for two things — the read-only verdict here, and command-prefix extraction at `_bash_parser.py:617` — so if the prefix-extraction use genuinely needs `tee`, the safer shape is to keep `SAFE_COMMANDS` for prefix extraction and decide read-only from `READ_ONLY_COMMANDS` alone, which already does not contain `tee`:
```python
# _is_single_command_read_only(): drop the SAFE_COMMANDS fallback, or
# exclude writing commands from it explicitly
WRITING_COMMANDS = {"tee"}
if base_cmd in SAFE_COMMANDS and base_cmd not in WRITING_COMMANDS:
return True
```
I have not implemented anything yet — per CONTRIBUTING I would like your steer on the shape first, since it touches the classifier that #2470 and #2500 are also about, and I do not want to collide with #2471.
### Environment
- AgentScope Version: 2.0.8
- Python Version: 3.13.15
- OS: Windows 11
コントリビューションガイド
調査の方向性
Start in src/agentscope/tool/_builtin/_bash_parser.py, reading SAFE_COMMANDS, READ_ONLY_COMMANDS, _is_single_command_read_only(), and the prefix-extraction use near line 617. Run the listed tee and redirection reproductions, then verify that tee writes are no longer classified read-only, deny rules reach the permission decision, and prefix extraction remains correct.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- python
- 領域
- cli, security
- issue の種類
- バグ
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 活発さ
- 活発
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 74/100