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:
Yusuf Suleman
2026-03-29 08:25:50 -05:00
parent d1801540ae
commit 6bd23e7e8b
8 changed files with 111 additions and 27 deletions

View File

@@ -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))