Title: Restricted local-runtime workspace boundary bypass via hardlink alias in AstrBot filesystem computer-use tools
Description:
A non-admin local-runtime user can bypass AstrBot's intended workspace-only filesystem restriction by operating on an in-workspace hardlink that aliases an out-of-workspace file. This allows unauthorized read and overwrite of host files that are outside the user's allowed workspace boundary but reside on the same filesystem and are accessible to the AstrBot process.
AstrBot exposes local computer-use filesystem tools including astrbot_file_read_tool and astrbot_file_write_tool. In restricted local mode (provider_settings.computer_use_require_admin=True and user role member), these tools are intended to limit access to approved roots such as data/workspaces/{normalized_umo}.
The boundary check in astrbot/core/tools/computer_tools/fs.py is based on resolved pathname containment, not on the identity of the final file object. _is_path_within_allowed_roots() resolves the requested path and authorizes it if the resolved pathname is under an allowed root:
def _is_path_within_allowed_roots(path: str, *, umo: str, allowed_roots: tuple[Path, ...]) -> bool:
resolved = _resolve_user_path(path, local_env=True, umo=umo)
return any(
resolved == allowed_root or resolved.is_relative_to(allowed_root)
for allowed_root in allowed_roots
)_normalize_rw_path() then accepts the path and returns it to the read/write sinks without checking whether the approved path is a hardlink to an inode outside the workspace:
normalized_path = _resolve_tool_path(path, local_env=local_env, umo=umo)
...
if restricted and not _is_path_within_allowed_roots(...):
raise PermissionError(...)
return normalized_pathFileReadTool.call() and FileWriteTool.call() both rely on that check and then pass the approved path to the real local filesystem backend:
normalized_path = _normalize_rw_path(...)
...
return await read_file_tool_result(..., path=normalized_path, ...)normalized_path = _normalize_rw_path(..., write=True)
...
result = await sb.fs.write_file(path=normalized_path, content=content, mode="w", encoding="utf-8")This creates a policy gap: a user can present a seemingly valid workspace path such as linked.txt, where linked.txt is a hardlink inside the workspace pointing to the same inode as an out-of-workspace file. The pathname passes authorization, but the subsequent file operation affects the outside file.
I verified this with AstrBot's real shipped FileReadTool, FileWriteTool, and LocalBooter backend in restricted local mode. A direct path such as ../outside/secret.txt is denied, while linked.txt inside the workspace is accepted and successfully overwrites the out-of-workspace file because both paths share the same inode.
- AstrBot running in local runtime mode
provider_settings.computer_use_require_admin=True- Attacker-controlled session role is
member, notadmin - The workspace directory and target file are on the same filesystem
- The AstrBot process has read and/or write permission to the target out-of-workspace file
- Python 3 environment capable of importing the local AstrBot repository
- Download the verification driver from: verification_test.py
- Download the shared helper from: common_hardlink_repro.py
- Download the control script from: control-direct-outside-path.py
- Place the three files in the same directory, or use the originals under
llm-enhance/cve-finding/similar/Path_File/Advisory-GHSA-3jx4-q2m7-r496-WorkspaceHardlinkAlias-exp/. - From the AstrBot repository root, run:
python3 llm-enhance/cve-finding/similar/Path_File/Advisory-GHSA-3jx4-q2m7-r496-WorkspaceHardlinkAlias-exp/verification_test.py - Observe that the script first confirms direct access to
../outside/secret.txtis denied, then createslinked.txtas a hardlink inside the workspace to the outside file, and finally reads and overwrites the outside file through the workspace alias. - Run the control case:
python3 llm-enhance/cve-finding/similar/Path_File/Advisory-GHSA-3jx4-q2m7-r496-WorkspaceHardlinkAlias-exp/control-direct-outside-path.py - Confirm that the direct outside path remains blocked and the outside file is unchanged in the control case.
Verification run:
[DEFECT-CONFIRMED-WITH-LIMITATIONS]
{
"mode": "Integration-Test",
"workspace": "/tmp/tmpkjyjhup0/workspaces/qq_friend_hardlink",
"outside_file": "/tmp/tmpkjyjhup0/outside/secret.txt",
"hardlink_path": "/tmp/tmpkjyjhup0/workspaces/qq_friend_hardlink/linked.txt",
"control_read": "Error: Read access is restricted for this user. Allowed directories: data/skills, data/plugins/*/skills, data/workspaces/qq_friend_hardlink, /tmp/.astrbot, /tmp/tmpkjyjhup0/temp. Blocked path: /tmp/tmpkjyjhup0/workspaces/outside/secret.txt.",
"control_write": "Error: Write access is restricted for this user. Allowed directories: data/skills, data/workspaces/qq_friend_hardlink, /tmp/.astrbot, /tmp/tmpkjyjhup0/temp. Blocked path: /tmp/tmpkjyjhup0/workspaces/outside/secret.txt.",
"vuln_read": "hardlink-canary-9f9a86f0\n",
"vuln_write": "File written successfully: /tmp/tmpkjyjhup0/workspaces/qq_friend_hardlink/linked.txt",
"outside_text_after": "pwned-via-hardlink\n",
"same_inode": true,
"outside_nlink": 2,
"mtime_changed": false,
"label": "[DEFECT-CONFIRMED-WITH-LIMITATIONS]",
"observation": "Outside file content changed after writing the in-workspace hardlink alias."
}
Control run:
{
"mode": "Integration-Test",
"control_read": "Error: Read access is restricted for this user. Allowed directories: data/skills, data/plugins/*/skills, data/workspaces/qq_friend_hardlink, /tmp/.astrbot, /tmp/tmp2q7jpmbx/temp. Blocked path: /tmp/tmp2q7jpmbx/workspaces/outside/secret.txt.",
"control_write": "Error: Write access is restricted for this user. Allowed directories: data/skills, data/workspaces/qq_friend_hardlink, /tmp/.astrbot, /tmp/tmp2q7jpmbx/temp. Blocked path: /tmp/tmp2q7jpmbx/workspaces/outside/secret.txt.",
"outside_text_after": "control-canary\n"
}
This is an authorization and path-boundary bypass affecting AstrBot's restricted local filesystem tool model. A non-admin member can read or overwrite files outside the intended workspace boundary if those files can be hardlinked into the workspace and the AstrBot process has filesystem permission to access them. This compromises confidentiality and integrity of host-local files reachable under the AstrBot runtime account. The issue does not automatically imply full host compromise, but it does break the product's documented security boundary for restricted local members.
- Ecosystem: pip
- Package name: AstrBot
- Affected versions: <= 4.25.5
- Patched versions:
- Severity: Medium
- Vector string: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- CWE: CWE-59: Improper Link Resolution Before File Access ('Link Following')
| Permalink | Description |
|---|---|
| https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/core/tools/computer_tools/fs.py#L166-L205 | The core authorization logic only checks whether the resolved pathname is under an allowed root and returns the path without validating whether the final file object is a hardlink alias to an out-of-workspace inode. |
| https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/core/tools/computer_tools/fs.py#L267-L295 | FileReadTool.call() relies on _normalize_rw_path() and then passes the approved path directly into the real filesystem read path, enabling unauthorized reads through a workspace hardlink alias. |
| https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/core/tools/computer_tools/fs.py#L334-L356 | FileWriteTool.call() performs the same flawed authorization flow for writes and then forwards the approved path to sb.fs.write_file, enabling overwrite of the outside file through the hardlink alias. |