Files
ledgrab/server/tests/e2e/test_backup_security.py
alexei.dolgolyov 123da1b5c4
Build Android APK / build-android (push) Failing after 1m45s
Lint & Test / test (push) Successful in 4m54s
fix: comprehensive security, stability, and code quality audit
Security:
- Force API key auth for LAN (non-loopback) requests; remove shipped dev key
- Block path-traversal in backup restore; require auth on backup endpoints
- SSRF protection: DNS resolve + private/loopback/link-local IP rejection
- AES-256-GCM encryption for HA tokens and MQTT passwords with auto-migration
- WebSocket auth migrated from query-string to first-message protocol
- Asset upload: extension allowlist, server-side mime, Content-Disposition
- Update installer: SHA256 verification, tar/zip member validation
- Tightened CORS (explicit methods/headers, no credentials)
- ADB serial regex allowlist, webhook rate-limit key fix, log scrubbing

Android:
- Root-capture: ordered teardown, screenrecord respawn watchdog, child reaping
- USB permission blocking API via CompletableDeferred
- Python init crash guard with fatal-error screen
- Moved root grant + QR generation off Main thread
- Cached PyObject engine for per-frame bridge calls
- Ordered ScreenCapture resource cleanup, allowBackup=false

Python:
- Replaced all asyncio.get_event_loop() with get_running_loop/to_thread
- Split color_strip_sources.py (1683->5 files) and color_strip_stream.py
  (1324->7 files) into packages
- Extracted FrameLimiter utility, migrated 9 stream loops
- Provider base-class reuse, WLED state caching + URL normalization
- Narrowed broad except-pass in WS routes, threading fixes in BaseStore

Frontend:
- XSS fix: escapeHtml on dynamic option labels, reconcile-based list renders
- Typed DOM helpers, safe localStorage access, AbortController listener hygiene
- openAuthedWs helper for first-message WS auth protocol
- Migrated remaining plain <select>s to IconSelect/EntitySelect

Design:
- WCAG AA primary color on light theme (#2e7d32, 5.4:1 contrast)
- Android TV 10-foot breakpoint (tv.css)
- Consolidated z-index tokens, unified easing, card-running GPU hints
2026-04-16 04:56:04 +03:00

50 lines
1.5 KiB
Python

"""Security tests for backup/restore endpoints (e2e — uses live app)."""
import io
import zipfile
from tests.e2e.conftest import API_KEY # type: ignore
def test_restore_rejects_path_traversal_zip(client):
"""ZIP entries with ../ must be rejected."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr(
"ledgrab.db",
b"SQLite format 3\x00" + b"\x00" * 200,
)
zf.writestr("assets/../evil.txt", b"bad")
buf.seek(0)
resp = client.post(
"/api/v1/system/restore",
files={"file": ("backup.zip", buf.getvalue(), "application/zip")},
)
assert resp.status_code == 400
assert "traversal" in resp.text.lower()
def test_backup_download_requires_non_anonymous(client):
"""Backup download rejects anonymous (loopback) callers."""
import ledgrab.config as cm
from ledgrab.config import AuthConfig
saved = cm.config
cm.config = saved.model_copy(update={"auth": AuthConfig(api_keys={})})
try:
client.headers.pop("Authorization", None)
resp = client.get("/api/v1/system/backup")
assert resp.status_code == 401
finally:
cm.config = saved
client.headers["Authorization"] = f"Bearer {API_KEY}"
def test_backup_roundtrip_authenticated(client):
"""A normal authenticated download still works."""
resp = client.get("/api/v1/system/backup")
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/zip"
assert resp.content[:4] == b"PK\x03\x04"