agentscope-ai / agentscope-ai/agentscope

Unconfined source path in workspace add_skill copies arbitrary server directories into the agent workspace (server-side file disclosure)

未关闭
#2,069 3 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
state: under discussion
主要语言
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 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。