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:
2026-04-16 03:21:45 +03:00
parent 734e5c9340
commit f0739ca949
30 changed files with 567 additions and 105 deletions
@@ -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}