Fix Tailscale Remote Control CSRF origins

Normalize active Remote Control URLs to same-origin values before adding them to CSRF allowlists, so Tailscale Funnel URLs with paths or trailing slashes can bootstrap tokens correctly.

Allow WebSocket origin validation to trust only the currently active Remote Control origin, including Docker split-process tunnel service URLs, while preserving rejection for unrelated external origins.

Add focused regression coverage for active Tailscale-style origins, tunnel-service origin lookup, and negative cross-origin cases; keep run_ui decorator re-exports compatible with existing CSRF tests.
This commit is contained in:
Alessandro 2026-06-04 14:52:49 +02:00
parent 85e28d0799
commit ca4efe6e6a
6 changed files with 320 additions and 9 deletions

View file

@ -1,5 +1,4 @@
import secrets
from urllib.parse import urlparse
from helpers.api import (
ApiHandler,
Input,
@ -9,6 +8,7 @@ from helpers.api import (
session,
)
from helpers import runtime, dotenv, login
from helpers.tunnel_origins import origin_from_url
import fnmatch
ALLOWED_ORIGINS_KEY = "ALLOWED_ORIGINS"
@ -82,11 +82,7 @@ class GetCsrfToken(ApiHandler):
)
if not r:
return None
# parse and normalize
p = urlparse(r)
if not p.scheme or not p.hostname:
return None
return f"{p.scheme}://{p.hostname}" + (f":{p.port}" if p.port else "")
return origin_from_url(r)
async def get_allowed_origins(self) -> list[str]:
# get the allowed origins from the environment
@ -107,8 +103,10 @@ class GetCsrfToken(ApiHandler):
from api.tunnel_proxy import process as tunnel_api_process
tunnel = await tunnel_api_process({"action": "get"})
if tunnel and isinstance(tunnel, dict) and tunnel["success"]:
allowed_origins.append(tunnel["tunnel_url"])
if tunnel and isinstance(tunnel, dict) and tunnel.get("success"):
tunnel_origin = origin_from_url(tunnel.get("tunnel_url"))
if tunnel_origin:
allowed_origins.append(tunnel_origin)
except Exception:
pass