Merge pull request #110513 from NousResearch/security/fs-read-sensitive-guard

fix(dashboard): apply the sensitive-path guard
This commit is contained in:
Jeffrey Quesnelle
2026-09-14 00:14:31 -04:00
committed by GitHub
2 changed files with 30 additions and 5 deletions
+3 -5
View File
@@ -169,6 +169,8 @@ def _fs_regular_file(path: Path) -> tuple[Path, os.stat_result]:
raise HTTPException(status_code=400, detail="Path points to a directory")
if not stat.S_ISREG(st.st_mode):
raise HTTPException(status_code=400, detail="Only regular files can be read")
if _is_sensitive_path(target):
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
return target, st
@@ -611,7 +613,7 @@ async def fs_list(path: str):
entries = []
with os.scandir(target) as scan:
for entry in scan:
if entry.name in _FS_READDIR_HIDDEN:
if entry.name in _FS_READDIR_HIDDEN or _is_sensitive_path(Path(entry.path)):
continue
entries.append({
"name": entry.name,
@@ -710,8 +712,6 @@ async def fs_read_data_url(
):
from hermes_cli.web_server import _FS_DATA_URL_MAX_BYTES
target, st = _fs_regular_file(await _fs_download_path(path, profile, session_id))
if _is_sensitive_path(target):
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
if st.st_size > _FS_DATA_URL_MAX_BYTES:
raise HTTPException(status_code=413, detail="File too large")
encoded = base64.b64encode(_fs_read_bytes(target)).decode("ascii")
@@ -723,8 +723,6 @@ async def fs_download(
path: str, profile: Optional[str] = None, session_id: Optional[str] = None,
):
target, _st = _fs_regular_file(await _fs_download_path(path, profile, session_id))
if _is_sensitive_path(target):
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
return FileResponse(
path=str(target),
media_type=_fs_mime_type(target),
+27
View File
@@ -77,6 +77,33 @@ def test_fs_download_rejects_sensitive_files(client, tmp_path):
assert response.status_code == 403
@pytest.mark.parametrize("endpoint", ["/api/fs/read-text", "/api/fs/read-data-url", "/api/fs/download"])
@pytest.mark.parametrize("relative", [".env", "auth.json", "mcp-tokens/github.json"])
def test_fs_readers_reject_sensitive_paths(client, tmp_path, endpoint, relative):
target = tmp_path / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("SECRET=1")
response = client.get(endpoint, params={"path": str(target)})
assert response.status_code == 403
assert "SECRET" not in response.text
def test_fs_list_hides_sensitive_entries(client, tmp_path):
root = tmp_path / "project"
root.mkdir()
(root / ".env").write_text("SECRET=1")
(root / "auth.json").write_text("{}")
(root / "mcp-tokens").mkdir()
(root / "notes.txt").write_text("ok")
response = client.get("/api/fs/list", params={"path": str(root)})
assert response.status_code == 200
assert [entry["name"] for entry in response.json()["entries"]] == ["notes.txt"]
def test_fs_endpoints_require_auth(tmp_path):
client = TestClient(web_server.app)
target = tmp_path / "secret.txt"