Change host normalization to remove port numbers - sessions are per host, cookies don't respect port numbers.

This commit is contained in:
2026-02-05 19:04:40 +00:00
parent 615066a2a2
commit c3df6c318c
3 changed files with 15 additions and 15 deletions
+6 -7
View File
@@ -455,8 +455,11 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
except KeyError: except KeyError:
return None return None
# Normalize host for comparison (stored hosts are already normalized)
normalized_input = normalize_host(host)
# Validate host matches (sessions are always created with a host) # Validate host matches (sessions are always created with a host)
if s.host != host: if s.host != normalized_input:
# Session bound to different host # Session bound to different host
return None return None
@@ -471,10 +474,6 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
# Effective permissions: role's permissions that the org can grant # Effective permissions: role's permissions that the org can grant
# Also filter by domain if host is provided # Also filter by domain if host is provided
org_perm_uuids = {p.uuid for p in org.permissions} org_perm_uuids = {p.uuid for p in org.permissions}
normalized_host = normalize_host(host)
host_without_port = (
normalized_host.rsplit(":", 1)[0] if normalized_host else None
)
effective_perms = [] effective_perms = []
for perm_uuid in role.permission_set: for perm_uuid in role.permission_set:
@@ -484,8 +483,8 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
p = self.permissions[perm_uuid] p = self.permissions[perm_uuid]
except KeyError: except KeyError:
continue continue
# Check domain restriction # Check domain restriction (normalized_input already has port stripped)
if p.domain is not None and p.domain != host_without_port: if p.domain is not None and p.domain != normalized_input:
continue continue
effective_perms.append(p) effective_perms.append(p)
+2 -2
View File
@@ -7,7 +7,7 @@ from paskia.fastapi.session import AUTH_COOKIE, infodict
from paskia.fastapi.wschat import authenticate_and_login, register_chat from paskia.fastapi.wschat import authenticate_and_login, register_chat
from paskia.fastapi.wsutil import validate_origin, websocket_error_handler from paskia.fastapi.wsutil import validate_origin, websocket_error_handler
from paskia.globals import passkey from paskia.globals import passkey
from paskia.util import passphrase from paskia.util import hostutil, passphrase
# Create a FastAPI subapp for WebSocket endpoints # Create a FastAPI subapp for WebSocket endpoints
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None) app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
@@ -31,7 +31,7 @@ async def websocket_register_add(
- Reset token supplied as ?reset=... (auth cookie ignored) - Reset token supplied as ?reset=... (auth cookie ignored)
""" """
origin = validate_origin(ws) origin = validate_origin(ws)
host = origin.split("://", 1)[1] host = hostutil.normalize_host(origin.split("://", 1)[1])
if reset is not None: if reset is not None:
if not passphrase.is_well_formed(reset): if not passphrase.is_well_formed(reset):
raise ValueError( raise ValueError(
+7 -6
View File
@@ -56,7 +56,7 @@ def reload_config() -> None:
def normalize_host(raw_host: str | None) -> str | None: def normalize_host(raw_host: str | None) -> str | None:
"""Normalize a Host header preserving port (exact match required).""" """Normalize a Host header, stripping port numbers for consistent matching."""
if not raw_host: if not raw_host:
return None return None
candidate = raw_host.strip() candidate = raw_host.strip()
@@ -65,11 +65,12 @@ def normalize_host(raw_host: str | None) -> str | None:
# urlsplit to parse (add // for scheme-less); prefer netloc to retain port. # urlsplit to parse (add // for scheme-less); prefer netloc to retain port.
parsed = urlsplit(candidate if "//" in candidate else f"//{candidate}") parsed = urlsplit(candidate if "//" in candidate else f"//{candidate}")
netloc = parsed.netloc or parsed.path or "" netloc = parsed.netloc or parsed.path or ""
# Strip IPv6 brackets around host part but retain port suffix. # Handle IPv6 addresses: [ipv6]:port or [ipv6]
if netloc.startswith("["): if netloc.startswith("["):
# format: [ipv6]:port or [ipv6]
if "]" in netloc: if "]" in netloc:
host_part, _, rest = netloc.partition("]") host_part, _, _ = netloc.partition("]")
port_part = rest.lstrip(":") netloc = host_part.strip("[]")
netloc = host_part.strip("[]") + (f":{port_part}" if port_part else "") else:
# Strip port from host:port
netloc = netloc.rsplit(":", 1)[0]
return netloc.lower() or None return netloc.lower() or None