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
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""Tests for SSRF protection in safe_source.validate_image_url."""
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from ledgrab.utils.safe_source import validate_image_url
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
"http://127.0.0.1/",
|
||||
"http://127.0.0.1:8080/x.png",
|
||||
"http://[::1]/",
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
"http://10.0.0.1/",
|
||||
"http://192.168.1.1/x.png",
|
||||
"http://172.16.0.5/",
|
||||
"http://0.0.0.0/",
|
||||
"http://224.0.0.1/", # multicast
|
||||
"ftp://example.com/",
|
||||
"file:///etc/passwd",
|
||||
"gopher://example.com/",
|
||||
"http:///no-host",
|
||||
],
|
||||
)
|
||||
def test_validate_image_url_rejects_dangerous_targets(url):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_image_url(url)
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
def test_validate_image_url_accepts_public(monkeypatch):
|
||||
"""Accept a public host whose DNS resolves to a non-private IP."""
|
||||
import ledgrab.utils.safe_source as ss
|
||||
|
||||
def fake_getaddrinfo(host, _port):
|
||||
# Pretend example.com resolves to a public IP
|
||||
return [(0, 0, 0, "", ("93.184.216.34", 0))]
|
||||
|
||||
monkeypatch.setattr(ss.socket, "getaddrinfo", fake_getaddrinfo)
|
||||
# Should not raise
|
||||
validate_image_url("https://example.com/image.png")
|
||||
|
||||
|
||||
def test_validate_image_url_blocks_dns_to_private(monkeypatch):
|
||||
"""Hostname that resolves to a private IP must be rejected."""
|
||||
import ledgrab.utils.safe_source as ss
|
||||
|
||||
def fake_getaddrinfo(host, _port):
|
||||
return [(0, 0, 0, "", ("10.0.0.5", 0))]
|
||||
|
||||
monkeypatch.setattr(ss.socket, "getaddrinfo", fake_getaddrinfo)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_image_url("http://internal.example.com/")
|
||||
assert exc.value.status_code == 400
|
||||
Reference in New Issue
Block a user