fix: harden dashboard auth and terminal flows
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 3m29s
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 3m29s
Tighten terminal websocket auth and proxy trust handling while making file-backed auth/RBAC writes atomic to reduce high-impact security and persistence risks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+48
-30
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
import ipaddress
|
||||
from fastapi import Request
|
||||
# Dashboard v1.3 — Runner DNS fix applied
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "http://gitea:3000")
|
||||
@@ -63,43 +65,59 @@ OPENCLAW_GATEWAY_TOKEN = os.environ.get("OPENCLAW_GATEWAY_TOKEN", "")
|
||||
|
||||
# Networking configs
|
||||
LAN_IP_RANGES = os.environ.get("LAN_IP_RANGES", "192.168.31.0/24").split(",")
|
||||
TRUSTED_PROXIES = os.environ.get("TRUSTED_PROXIES", "127.0.0.1,::1,172.21.0.1").split(",")
|
||||
|
||||
|
||||
def _parse_ip(ip_str: str):
|
||||
try:
|
||||
return ipaddress.ip_address((ip_str or "").strip())
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _ip_matches_ranges(ip_str: str, ranges: list[str]) -> bool:
|
||||
ip_obj = _parse_ip(ip_str)
|
||||
if ip_obj is None:
|
||||
return False
|
||||
|
||||
for subnet_str in ranges:
|
||||
subnet_str = subnet_str.strip()
|
||||
if not subnet_str:
|
||||
continue
|
||||
try:
|
||||
if "/" in subnet_str:
|
||||
if ip_obj in ipaddress.ip_network(subnet_str, strict=False):
|
||||
return True
|
||||
elif ip_obj == ipaddress.ip_address(subnet_str):
|
||||
return True
|
||||
except ValueError:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def is_lan_ip(ip_str: str) -> bool:
|
||||
"""Check if IP is in local LAN (not Tailscale)"""
|
||||
import ipaddress
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(ip_str)
|
||||
for subnet_str in LAN_IP_RANGES:
|
||||
if ip_obj in ipaddress.ip_network(subnet_str.strip()):
|
||||
return True
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
return _ip_matches_ranges(ip_str, LAN_IP_RANGES)
|
||||
|
||||
|
||||
def is_tailscale_ip(ip_str: str) -> bool:
|
||||
"""Check if IP is in Tailscale CGNAT range (100.64.0.0/10)"""
|
||||
import ipaddress
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(ip_str)
|
||||
return ip_obj in ipaddress.ip_network("100.64.0.0/10")
|
||||
except ValueError:
|
||||
return False
|
||||
return _ip_matches_ranges(ip_str, ["100.64.0.0/10"])
|
||||
|
||||
TRUSTED_PROXIES = os.environ.get("TRUSTED_PROXIES", "127.0.0.1,::1,172.16.0.0/12").split(",")
|
||||
|
||||
def is_trusted_proxy(ip_str: str) -> bool:
|
||||
import ipaddress
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(ip_str)
|
||||
for subnet_str in TRUSTED_PROXIES:
|
||||
subnet_str = subnet_str.strip()
|
||||
if "/" in subnet_str:
|
||||
if ip_obj in ipaddress.ip_network(subnet_str):
|
||||
return True
|
||||
else:
|
||||
if ip_obj == ipaddress.ip_address(subnet_str):
|
||||
return True
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
return _ip_matches_ranges(ip_str, TRUSTED_PROXIES)
|
||||
|
||||
|
||||
def get_forwarded_client_ip(request: Request) -> str | None:
|
||||
source_ip = request.client.host if request.client else ""
|
||||
if not is_trusted_proxy(source_ip):
|
||||
return None
|
||||
|
||||
forwarded_for = request.headers.get("x-forwarded-for", "")
|
||||
forwarded_ip = forwarded_for.split(",", 1)[0].strip()
|
||||
return forwarded_ip or None
|
||||
|
||||
|
||||
def get_client_ip(request: Request) -> str:
|
||||
return get_forwarded_client_ip(request) or (request.client.host if request.client else "")
|
||||
|
||||
Reference in New Issue
Block a user