fix(dashboard): use pattern match for .env sensitive file guard
Replace the exact-filename frozenset with _is_sensitive_filename() that matches .env plus any .env.<suffix> variant. This covers shorthand suffixes like .env.prod that the previous enumeration missed. Add test_sensitive_env_suffix_variants_blocked regression test covering .env.prod, .env.dev, .env.staging.local, and .env.ci. Addresses review feedback from egilewski on PR #57507.
This commit is contained in:
parent
bc55c201c7
commit
1bcc52c14e
2 changed files with 22 additions and 13 deletions
|
|
@ -1191,15 +1191,9 @@ _FS_READDIR_HIDDEN = {
|
|||
# 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",
|
||||
})
|
||||
def _is_sensitive_filename(name: str) -> bool:
|
||||
"""Return True for ``.env`` and any ``.env.<suffix>`` variant."""
|
||||
return name == ".env" or name.startswith(".env.")
|
||||
_FS_DATA_URL_MAX_BYTES = 16 * 1024 * 1024
|
||||
_FS_TEXT_SOURCE_MAX_BYTES = 64 * 1024 * 1024
|
||||
_FS_TEXT_PREVIEW_MAX_BYTES = 512 * 1024
|
||||
|
|
@ -1633,7 +1627,7 @@ async def list_managed_files(request: Request, path: Optional[str] = None):
|
|||
entries = [
|
||||
_managed_file_entry(policy, child)
|
||||
for child in target.iterdir()
|
||||
if child.name not in _SENSITIVE_FILENAMES
|
||||
if not _is_sensitive_filename(child.name)
|
||||
]
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail="Directory is not readable")
|
||||
|
|
@ -1660,7 +1654,7 @@ 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:
|
||||
if _is_sensitive_filename(target.name):
|
||||
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
|
||||
|
||||
try:
|
||||
|
|
@ -1704,7 +1698,7 @@ 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:
|
||||
if _is_sensitive_filename(target.name):
|
||||
raise HTTPException(status_code=403, detail="Access to sensitive files is not allowed")
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ 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.
|
||||
# Create a regular file and .env variants including shorthand suffixes.
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
regular = root / "config.txt"
|
||||
regular.write_text("safe content")
|
||||
|
|
@ -502,6 +502,8 @@ def test_sensitive_env_files_hidden_from_listing(forced_files_client):
|
|||
env_file.write_text("SECRET_KEY=abc123")
|
||||
env_local = root / ".env.local"
|
||||
env_local.write_text("LOCAL_SECRET=def456")
|
||||
env_prod = root / ".env.prod"
|
||||
env_prod.write_text("PROD_SECRET=ghi789")
|
||||
|
||||
listing = client.get("/api/files", params={"path": str(root)})
|
||||
assert listing.status_code == 200
|
||||
|
|
@ -509,6 +511,7 @@ def test_sensitive_env_files_hidden_from_listing(forced_files_client):
|
|||
assert "config.txt" in names
|
||||
assert ".env" not in names
|
||||
assert ".env.local" not in names
|
||||
assert ".env.prod" not in names
|
||||
|
||||
|
||||
def test_sensitive_env_files_blocked_read(forced_files_client):
|
||||
|
|
@ -533,3 +536,15 @@ def test_sensitive_env_files_blocked_download(forced_files_client):
|
|||
|
||||
resp = client.get("/api/files/download", params={"path": str(env_file)})
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_sensitive_env_suffix_variants_blocked(forced_files_client):
|
||||
"""Regression: .env.<suffix> shorthand variants (e.g. .env.prod) must also be blocked."""
|
||||
client, root = forced_files_client
|
||||
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
for suffix in ("prod", "dev", "staging.local", "ci"):
|
||||
p = root / f".env.{suffix}"
|
||||
p.write_text(f"SECRET_{suffix}=abc123")
|
||||
assert client.get("/api/files/read", params={"path": str(p)}).status_code == 403
|
||||
assert client.get("/api/files/download", params={"path": str(p)}).status_code == 403
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue