agentscope-ai / agentscope-ai/agentscope
Unconfined source path in workspace add_skill copies arbitrary server directories into the agent workspace (server-side file disclosure)
- 主要语言
- Python
- 星标
- 31.5k
- 派生
- 3.5k
- 平均合并
- 1 天 23 小时
- 30 天内合并 PR
- 95
描述
### Prerequisites
- [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
reported on 12 June 2026 https://github.com/agentscope-ai/agentscope/security/advisories/GHSA-2j4r-4qmg-x5h9 - no response, so trying here:
### Summary
The AgentScope workspace `add_skill` operation copies a caller-supplied source directory into the agent's skills directory using `shutil.copytree`. The destination is confined to the skills directory, but the source path is taken directly from the request body and is never confined. A caller can therefore copy an arbitrary server-side directory (any directory that contains, or into which the caller can plant, a valid `SKILL.md`) into the agent-readable workspace and read its contents back through the workspace skill listing. The HTTP API has no authentication (identity is a client-supplied header). Confirmed against the `LocalWorkspace.add_skill`: an arbitrary source directory was copied into the workspace.
### Details
`src/agentscope/workspace/_local_workspace.py`, `LocalWorkspace.add_skill` (method around line 934, sink around lines 1006 to 1007):
```python
# dest_path is confined to skills_dir (realpath startswith check, ~999-1003)
shutil.copytree(skill_path, dest_path, dirs_exist_ok=False) # source skill_path NOT confined
```
`skill_path` flows from the request body: `AddSkillRequest.skill_path` (`src/agentscope/app/_router/_workspace.py` ~line 23) is passed straight to `await workspace.add_skill(body.skill_path)` (~line 197). There is no validation that the source is inside any allowed root. The only constraint is `_validate_and_hash_skill` (~line 428), which requires the source directory to contain a `SKILL.md` with `name` and `description` frontmatter; the rest of the directory's files are copied verbatim.
Authentication is a placeholder: `src/agentscope/app/deps.py` `get_current_user_id` (~lines 17 to 39) reads an `X-User-ID` header (docstring: "Temporary header-based identity; will be replaced by JWT auth"), so any caller supplies their own identity, and the official example binds `0.0.0.0`.
### Error Messages
```shell
Affected version: 2.0.1
```
### Steps to Reproduce
### PoC
Plant an outside-the-workspace directory containing a `SKILL.md` (the only constraint - the source must look like a skill) plus a sentinel secret, then have the real workspace manager copy it in. Source needs a `SKILL.md`; the destination check is present but the source path is honored unconfined.
```bash
# 1. prepare an attacker-readable secret directory OUTSIDE any workspace
mkdir -p /tmp/secretdir
printf -- '---\nname: x\ndescription: y\n---\n' > /tmp/secretdir/SKILL.md
echo SECRET > /tmp/secretdir/stolen.txt
```
```python
# 2. call the add_skill (or POST /workspace/skill {"skill_path":"/tmp/secretdir"} with an X-User-ID header)
ws = LocalWorkspace(workdir="/tmp/agentscope_ws/user-abc/session-1")
await ws.initialize()
await ws.add_skill("/tmp/secretdir") # shutil.copytree(skill_path, dest) - skill_path NOT confined
```
```
# 3. HTTP form (auth is a placeholder X-User-ID header, deps.py:17-39):
POST /workspace/skill
X-User-ID: anyone
{"skill_path": "/tmp/secretdir"}
```
Validated against the `add_skill` (`_local_workspace.py:1006-1007` `shutil.copytree(skill_path, dest_path)`; dest confined at `:999-1003` via realpath/startswith, source `skill_path` from `AddSkillRequest.skill_path` not confined): the contents of `/tmp/secretdir` (the sentinel) were copied into `/tmp/agentscope_ws/user-abc/session-1/skills//`, readable via the workspace skills listing.
### Impact
A caller (unauthenticated, given the placeholder header identity) can copy arbitrary server-side directories into the agent-readable workspace and exfiltrate their contents, disclosing server files such as configuration, credentials directories, or other tenants' workspaces, subject only to the `SKILL.md` requirement (which the caller can satisfy by first planting a `SKILL.md` via the agent's own write capability, or by targeting directories that already contain one).
### Remediation
Confine the source `skill_path` to an allowed root the same way the destination is confined: resolve `skill_path` and assert it stays within an approved skills-source directory (reject absolute paths and `..`), before calling `shutil.copytree`. Do not accept an arbitrary server filesystem path from the request. Replace the placeholder `X-User-ID` identity with authentication before exposing the workspace API.
### Environment
-
贡献指南
评估
这个 Issue 还没有评估数据。