feat: security hardening — SSRF guard, template sandbox timeout, webhook log prune, auth & backup polish
- Add outbound URL validation (SSRF) for webhook/Discord/Slack/ntfy/Matrix dispatch - Template renderer: input/output caps and thread-based render timeout - Webhook log filter: strip Authorization/signature/token-like headers; atomic prune - Auth/JWT/backup/config tightening; misc frontend UX fixes
This commit is contained in:
@@ -31,6 +31,23 @@ def _backup_dir():
|
||||
return app_config.data_dir / "backups"
|
||||
|
||||
|
||||
def _resolve_backup_file(filename: str):
|
||||
"""Validate filename and resolve to a path strictly inside the backup dir."""
|
||||
if not filename.startswith("backup-") or not filename.endswith(".json"):
|
||||
raise HTTPException(status_code=404, detail="Backup file not found")
|
||||
if "/" in filename or "\\" in filename or ".." in filename or "\x00" in filename:
|
||||
raise HTTPException(status_code=404, detail="Backup file not found")
|
||||
base = _backup_dir().resolve()
|
||||
candidate = (base / filename).resolve()
|
||||
try:
|
||||
candidate.relative_to(base)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Backup file not found")
|
||||
if not candidate.is_file():
|
||||
raise HTTPException(status_code=404, detail="Backup file not found")
|
||||
return candidate
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Export
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -194,9 +211,7 @@ async def download_backup_file(
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Download a specific backup file."""
|
||||
filepath = _backup_dir() / filename
|
||||
if not filepath.is_file() or not filename.startswith("backup-"):
|
||||
raise HTTPException(status_code=404, detail="Backup file not found")
|
||||
filepath = _resolve_backup_file(filename)
|
||||
|
||||
try:
|
||||
content = json.loads(filepath.read_text(encoding="utf-8"))
|
||||
@@ -215,9 +230,6 @@ async def delete_backup_file(
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Delete a specific backup file."""
|
||||
filepath = _backup_dir() / filename
|
||||
if not filepath.is_file() or not filename.startswith("backup-"):
|
||||
raise HTTPException(status_code=404, detail="Backup file not found")
|
||||
|
||||
filepath = _resolve_backup_file(filename)
|
||||
filepath.unlink()
|
||||
return {"deleted": filename}
|
||||
|
||||
Reference in New Issue
Block a user