Change host normalization to remove port numbers - sessions are per host, cookies don't respect port numbers.
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user