Summary
The README's "Security Architecture" section claims filesystem.py provides a "Path safety check (blocks .. traversal)" and that multi_project.py uses "the same policy." In practice, both checks only reject relative ../ segments. An absolute path bypasses the check entirely in filesystem.py, and multi_project.py's cross-repo file tools have no path check at all. Any MCP client (or content an agent is tricked into acting on, e.g. prompt injection from a scraped page or malicious file) can read/write arbitrary files outside the intended project directory.
Affected files: filesystem.py, multi_project.py
Disclosure: Private vulnerability reporting is disabled on this repo (confirmed via gh api repos/hackerlibs/code-hacker/private-vulnerability-reporting → {"enabled":false}), so filing directly as a public issue.
Bug 1: filesystem.py::is_safe_path() — absolute-path bypass
def is_safe_path(path: str) -> bool:
"""Check if the path is safe (no directory traversal)."""
try:
resolved_path = Path(path).resolve()
return not any(part.startswith('..') for part in Path(path).parts)
except Exception:
return False
resolved_path is computed but never used — the actual check runs against Path(path).parts (the unresolved input), which only ever contains '..' for relative traversal syntax. An absolute path has no .. parts, so it passes unconditionally:
>>> is_safe_path("../../etc/passwd")
False # blocked, as intended
>>> is_safe_path("/etc/passwd")
True # NOT blocked — absolute path bypasses the check entirely
>>> is_safe_path("/home/otheruser/.ssh/id_rsa")
True
This function gates 9 of the 12 filesystem-command tools, including read_file, write_file, append_file, edit_file, list_directory, create_directory, find_files, search_files_ag, and the working_directory parameter of execute_command. Every one of them accepts an absolute path straight through to disk with no containment check against any project root.
Suggested fix
Actually use the resolved path, and check containment against an intended base directory:
def is_safe_path(path: str, base_dir: Path | None = None) -> bool:
try:
resolved = Path(path).resolve()
except Exception:
return False
if base_dir is None:
return True # no boundary configured — document this explicitly if intentional
try:
resolved.relative_to(base_dir.resolve())
return True
except ValueError:
return False
If the intent is genuinely "full filesystem access, just block .. cosmetically," the README's "Security Sandbox" claim should be corrected instead — right now it overstates what the check does.
Bug 2: multi_project.py — no path check at all on cross-repo file tools
workspace_read_file, workspace_write_file, and workspace_edit_file build the target path as:
full_path = Path(project_root) / file_path
with no call to is_safe_path or any other validation. Path.joinpath (/) discards the left side entirely when the right side is absolute:
>>> Path("/home/edneam/some_registered_project") / "/etc/shadow"
PosixPath('/etc/shadow')
So workspace_read_file(project="<any-registered-alias>", file_path="/etc/shadow") reads /etc/shadow directly — no .. needed.
It's worse than that, though: _resolve_project_path() doesn't require the project to be pre-registered in the workspace at all —
def _resolve_project_path(ws: dict, name_or_path: str) -> Optional[str]:
if name_or_path in ws["projects"]:
return ws["projects"][name_or_path]["path"]
if Path(name_or_path).is_dir():
return str(Path(name_or_path).resolve())
return None
— any existing directory string is accepted as project. So the minimal call is:
workspace_read_file(project="/", file_path="etc/shadow")
with zero setup and zero traversal syntax. workspace_write_file/workspace_edit_file are constrained somewhat by _is_allowed_file()'s extension allowlist (which still includes .env, .yaml, .json, .sh, etc.), but workspace_read_file has no extension check at all — any file the server process can read is exposed (SSH keys, cloud credential files, /etc/shadow if running as root, other users' home directories, etc).
Suggested fix
Require project to resolve via a registered alias only (drop the raw-directory fallback, or make it explicit/opt-in), and validate file_path the same way as Bug 1 — resolve Path(project_root) / file_path, then verify relative_to(project_root) before touching disk. Apply this to workspace_read_file as well, not just the write paths.
Why this matters for this specific project
These servers are designed to be driven by an LLM agent (VS Code Copilot Chat / DeepAgent / TUI), often on content the agent didn't choose — a fetched web page, a file in the repo it's reviewing, output from another tool. The README's own threat model (Security Sandbox — Each server has independent security policies (path checks, ...)) frames this as a boundary against exactly that: a compromised/injected instruction shouldn't be able to walk the agent outside the project it's supposed to be working in. Right now that boundary only blocks the most naive traversal syntax and is trivially sidestepped with an absolute path.
Environment
- Reviewed against
master @ current HEAD (2026-07-28 update)
- No live server needed to reproduce —
is_safe_path() and the Path join behavior are pure functions, reproducible directly:
from pathlib import Path
def is_safe_path(path: str) -> bool:
resolved_path = Path(path).resolve()
return not any(part.startswith('..') for part in Path(path).parts)
assert is_safe_path("/etc/passwd") is True # should be False
full_path = Path("/home/user/some_project") / "/etc/shadow"
assert str(full_path) == "/etc/shadow" # workspace_read_file target, no containment check
Happy to send a PR with the fix for both files if useful.
Summary
The README's "Security Architecture" section claims
filesystem.pyprovides a "Path safety check (blocks .. traversal)" and thatmulti_project.pyuses "the same policy." In practice, both checks only reject relative../segments. An absolute path bypasses the check entirely infilesystem.py, andmulti_project.py's cross-repo file tools have no path check at all. Any MCP client (or content an agent is tricked into acting on, e.g. prompt injection from a scraped page or malicious file) can read/write arbitrary files outside the intended project directory.Affected files:
filesystem.py,multi_project.pyDisclosure: Private vulnerability reporting is disabled on this repo (confirmed via
gh api repos/hackerlibs/code-hacker/private-vulnerability-reporting→{"enabled":false}), so filing directly as a public issue.Bug 1:
filesystem.py::is_safe_path()— absolute-path bypassresolved_pathis computed but never used — the actual check runs againstPath(path).parts(the unresolved input), which only ever contains'..'for relative traversal syntax. An absolute path has no..parts, so it passes unconditionally:This function gates 9 of the 12
filesystem-commandtools, includingread_file,write_file,append_file,edit_file,list_directory,create_directory,find_files,search_files_ag, and theworking_directoryparameter ofexecute_command. Every one of them accepts an absolute path straight through to disk with no containment check against any project root.Suggested fix
Actually use the resolved path, and check containment against an intended base directory:
If the intent is genuinely "full filesystem access, just block
..cosmetically," the README's "Security Sandbox" claim should be corrected instead — right now it overstates what the check does.Bug 2:
multi_project.py— no path check at all on cross-repo file toolsworkspace_read_file,workspace_write_file, andworkspace_edit_filebuild the target path as:with no call to
is_safe_pathor any other validation.Path.joinpath(/) discards the left side entirely when the right side is absolute:So
workspace_read_file(project="<any-registered-alias>", file_path="/etc/shadow")reads/etc/shadowdirectly — no..needed.It's worse than that, though:
_resolve_project_path()doesn't require the project to be pre-registered in the workspace at all —— any existing directory string is accepted as
project. So the minimal call is:with zero setup and zero traversal syntax.
workspace_write_file/workspace_edit_fileare constrained somewhat by_is_allowed_file()'s extension allowlist (which still includes.env,.yaml,.json,.sh, etc.), butworkspace_read_filehas no extension check at all — any file the server process can read is exposed (SSH keys, cloud credential files,/etc/shadowif running as root, other users' home directories, etc).Suggested fix
Require
projectto resolve via a registered alias only (drop the raw-directory fallback, or make it explicit/opt-in), and validatefile_paththe same way as Bug 1 — resolvePath(project_root) / file_path, then verifyrelative_to(project_root)before touching disk. Apply this toworkspace_read_fileas well, not just the write paths.Why this matters for this specific project
These servers are designed to be driven by an LLM agent (VS Code Copilot Chat / DeepAgent / TUI), often on content the agent didn't choose — a fetched web page, a file in the repo it's reviewing, output from another tool. The README's own threat model (
Security Sandbox — Each server has independent security policies (path checks, ...)) frames this as a boundary against exactly that: a compromised/injected instruction shouldn't be able to walk the agent outside the project it's supposed to be working in. Right now that boundary only blocks the most naive traversal syntax and is trivially sidestepped with an absolute path.Environment
master@ current HEAD (2026-07-28 update)is_safe_path()and thePathjoin behavior are pure functions, reproducible directly:Happy to send a PR with the fix for both files if useful.