fix: security hardening across platform
- Disable open /api/auth/register endpoint (gateway) - Require gateway session auth on Immich and Karakeep hooks proxies - Replace SHA-256 with bcrypt in fitness service (auth + seed) - Remove hardcoded Telegram user IDs from fitness seed - Add Secure flag to session cookie - Add domain allowlist and content-type validation to image proxy - Strengthen .gitignore (env variants, runtime data, test artifacts)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Platform Gateway — Image proxy (bypass hotlink protection).
|
||||
Platform Gateway — Image proxy with domain allowlist.
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
@@ -7,24 +7,65 @@ import urllib.parse
|
||||
|
||||
from config import _ssl_ctx
|
||||
|
||||
ALLOWED_IMAGE_DOMAINS = {
|
||||
"i.redd.it",
|
||||
"preview.redd.it",
|
||||
"external-preview.redd.it",
|
||||
"i.imgur.com",
|
||||
"imgur.com",
|
||||
"images-na.ssl-images-amazon.com",
|
||||
"m.media-amazon.com",
|
||||
"cdn.discordapp.com",
|
||||
"media.discordapp.net",
|
||||
"pbs.twimg.com",
|
||||
"abs.twimg.com",
|
||||
"upload.wikimedia.org",
|
||||
"images.unsplash.com",
|
||||
}
|
||||
|
||||
|
||||
def handle_image_proxy(handler):
|
||||
"""Proxy external images to bypass hotlink protection (e.g. Reddit)."""
|
||||
"""Proxy external images to bypass hotlink protection."""
|
||||
qs = urllib.parse.urlparse(handler.path).query
|
||||
params = urllib.parse.parse_qs(qs)
|
||||
url = params.get("url", [None])[0]
|
||||
if not url:
|
||||
handler._send_json({"error": "Missing url parameter"}, 400)
|
||||
return
|
||||
|
||||
# Validate URL scheme and domain allowlist
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
handler._send_json({"error": "Invalid URL scheme"}, 400)
|
||||
return
|
||||
hostname = parsed.hostname or ""
|
||||
if hostname not in ALLOWED_IMAGE_DOMAINS:
|
||||
allowed = any(
|
||||
hostname == d or hostname.endswith(f".{d}")
|
||||
for d in ALLOWED_IMAGE_DOMAINS
|
||||
)
|
||||
if not allowed:
|
||||
print(f"[ImageProxy] Blocked request for domain: {hostname}")
|
||||
handler._send_json({"error": "Domain not allowed"}, 403)
|
||||
return
|
||||
except Exception:
|
||||
handler._send_json({"error": "Invalid URL"}, 400)
|
||||
return
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={
|
||||
"User-Agent": "Mozilla/5.0 (compatible; PlatformProxy/1.0)",
|
||||
"Accept": "image/*,*/*",
|
||||
"Referer": urllib.parse.urlparse(url).scheme + "://" + urllib.parse.urlparse(url).netloc + "/",
|
||||
"Referer": parsed.scheme + "://" + parsed.netloc + "/",
|
||||
})
|
||||
resp = urllib.request.urlopen(req, timeout=10, context=_ssl_ctx)
|
||||
body = resp.read()
|
||||
ct = resp.headers.get("Content-Type", "image/jpeg")
|
||||
# Only serve actual image content types
|
||||
if not ct.startswith("image/"):
|
||||
handler._send_json({"error": "Response is not an image"}, 400)
|
||||
return
|
||||
handler.send_response(200)
|
||||
handler.send_header("Content-Type", ct)
|
||||
handler.send_header("Content-Length", len(body))
|
||||
|
||||
@@ -42,7 +42,7 @@ class ResponseMixin:
|
||||
|
||||
def _set_session_cookie(self, token):
|
||||
self.send_header("Set-Cookie",
|
||||
f"platform_session={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_MAX_AGE}")
|
||||
f"platform_session={token}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age={SESSION_MAX_AGE}")
|
||||
|
||||
def _read_body(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
|
||||
@@ -184,7 +184,7 @@ class GatewayHandler(ResponseMixin, BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
if path == "/api/auth/register":
|
||||
handle_register(self, body)
|
||||
self._send_json({"error": "Registration is disabled"}, 403)
|
||||
return
|
||||
|
||||
if path == "/api/me/connections":
|
||||
|
||||
Reference in New Issue
Block a user