mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix(dashboard): block .env files from managed-files API
The dashboard Files tab could list, read, and download .env files containing API keys when running with a bind-mounted Hermes home directory (e.g. docker run -v ~/.hermes:/opt/data). Add _SENSITIVE_FILENAMES frozenset and filter these from list_managed_files(), read_managed_file(), and download_managed_file(). Return 403 for direct read/download attempts on sensitive files. Fixes #57505
This commit is contained in:
parent
16332af60b
commit
bc55c201c7
2 changed files with 68 additions and 1 deletions
|
|
@ -1186,6 +1186,20 @@ _FS_READDIR_HIDDEN = {
|
|||
"target",
|
||||
"venv",
|
||||
}
|
||||
|
||||
# Filenames that must never be listed, read, or downloaded through the
|
||||
# managed-files API. These typically contain credentials (API keys, tokens)
|
||||
# and exposing them through the dashboard file browser is a security leak —
|
||||
# see issue #57505.
|
||||
_SENSITIVE_FILENAMES: frozenset[str] = frozenset({
|
||||
".env",
|
||||
".env.local",
|
||||
".env.production",
|
||||
".env.development",
|
||||
".env.staging",
|
||||
".env.test",
|
||||
".env.backup",
|
||||
})
|
||||
_FS_DATA_URL_MAX_BYTES = 16 * 1024 * 1024
|
||||
_FS_TEXT_SOURCE_MAX_BYTES = 64 * 1024 * 1024
|
||||
_FS_TEXT_PREVIEW_MAX_BYTES = 512 * 1024
|
||||
|
|
@ -1616,7 +1630,11 @@ async def list_managed_files(request: Request, path: Optional[str] = None):
|
|||
raise HTTPException(status_code=400, detail="Path is not a directory")
|
||||
|
||||
try:
|
||||
entries = [_managed_file_entry(policy, child) for child in target.iterdir()]
|
||||
entries = [
|
||||
_managed_file_entry(policy, child)
|
||||
for child in target.iterdir()
|
||||
if child.name not in _SENSITIVE_FILENAMES
|
||||
]
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail="Directory is not readable")
|
||||
except OSError as exc:
|
||||
|
|
@ -1642,6 +1660,8 @@ async def read_managed_file(request: Request, path: str):
|
|||
raise HTTPException(status_code=404, detail="File not found")
|
||||
if not target.is_file():
|
||||
raise HTTPException(status_code=400, detail="Path is not a file")
|
||||
if target.name in _SENSITIVE_FILENAMES:
|
||||
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
|
||||
|
||||
try:
|
||||
size = target.stat().st_size
|
||||
|
|
@ -1684,6 +1704,8 @@ async def download_managed_file(request: Request, path: str):
|
|||
raise HTTPException(status_code=404, detail="File not found")
|
||||
if not target.is_file():
|
||||
raise HTTPException(status_code=400, detail="Path is not a file")
|
||||
if target.name in _SENSITIVE_FILENAMES:
|
||||
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
|
||||
|
||||
try:
|
||||
size = target.stat().st_size
|
||||
|
|
|
|||
|
|
@ -488,3 +488,48 @@ def test_stream_upload_cleans_temp_on_cancellation(forced_files_client):
|
|||
# ... and no .upload temp file was left behind.
|
||||
leftovers = [p.name for p in target.parent.iterdir() if ".upload" in p.name]
|
||||
assert leftovers == [], f"temp upload files leaked on cancellation: {leftovers}"
|
||||
|
||||
|
||||
def test_sensitive_env_files_hidden_from_listing(forced_files_client):
|
||||
"""Regression test for #57505: .env files must not appear in directory listings."""
|
||||
client, root = forced_files_client
|
||||
|
||||
# Create a regular file and a .env file.
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
regular = root / "config.txt"
|
||||
regular.write_text("safe content")
|
||||
env_file = root / ".env"
|
||||
env_file.write_text("SECRET_KEY=abc123")
|
||||
env_local = root / ".env.local"
|
||||
env_local.write_text("LOCAL_SECRET=def456")
|
||||
|
||||
listing = client.get("/api/files", params={"path": str(root)})
|
||||
assert listing.status_code == 200
|
||||
names = [e["name"] for e in listing.json()["entries"]]
|
||||
assert "config.txt" in names
|
||||
assert ".env" not in names
|
||||
assert ".env.local" not in names
|
||||
|
||||
|
||||
def test_sensitive_env_files_blocked_read(forced_files_client):
|
||||
"""Regression test for #57505: .env files must not be readable."""
|
||||
client, root = forced_files_client
|
||||
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
env_file = root / ".env"
|
||||
env_file.write_text("SECRET_KEY=abc123")
|
||||
|
||||
resp = client.get("/api/files/read", params={"path": str(env_file)})
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_sensitive_env_files_blocked_download(forced_files_client):
|
||||
"""Regression test for #57505: .env files must not be downloadable."""
|
||||
client, root = forced_files_client
|
||||
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
env_file = root / ".env"
|
||||
env_file.write_text("SECRET_KEY=abc123")
|
||||
|
||||
resp = client.get("/api/files/download", params={"path": str(env_file)})
|
||||
assert resp.status_code == 403
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue