diff --git a/containers/auth-gateway/auth_gateway/services/resolver.py b/containers/auth-gateway/auth_gateway/services/resolver.py index 244461e..76340ac 100644 --- a/containers/auth-gateway/auth_gateway/services/resolver.py +++ b/containers/auth-gateway/auth_gateway/services/resolver.py @@ -1,3 +1,4 @@ +import posixpath from dataclasses import dataclass from urllib.parse import unquote, urlsplit @@ -22,7 +23,9 @@ def normalize_host(raw_host: str) -> str: def normalize_path(raw_uri: str) -> str: parsed = urlsplit(raw_uri or "/") path = unquote(parsed.path or "/") - return path if path.startswith("/") else f"/{path}" + path = path if path.startswith("/") else f"/{path}" + # Resolve any .. or . segments to prevent path traversal bypasses + return posixpath.normpath(path) def _path_matches(path: str, prefix: str) -> bool: diff --git a/containers/auth-gateway/auth_gateway/web/login_routes.py b/containers/auth-gateway/auth_gateway/web/login_routes.py index 8a1341c..96066a2 100644 --- a/containers/auth-gateway/auth_gateway/web/login_routes.py +++ b/containers/auth-gateway/auth_gateway/web/login_routes.py @@ -152,9 +152,9 @@ async def login_password_submit(request: Request, next: str = Form("/"), usernam if effective_policy.totp_method_names: logger.info("AUTH password ok for '%s' → totp required (policy: %s)", username, context.policy_name) - return _redirect_with_cookie(request, build_external_url(request, "/login/totp", next=next), session) + return _redirect_with_cookie(request, build_external_url(request, "/login/totp", next=context.path), session) logger.info("AUTH login ok for '%s' (policy: %s)", username, context.policy_name) - return _redirect_with_cookie(request, next, session) + return _redirect_with_cookie(request, context.path, session) @router.get("/login/totp", response_class=HTMLResponse) @@ -205,7 +205,7 @@ async def login_totp_submit(request: Request, next: str = Form("/"), token: str expires_in_minutes=get_effective_expiration(request, effective_policy, ["totp"]), ) logger.info("AUTH login ok for '%s' via password+totp (policy: %s)", refreshed_session.username, context.policy_name) - return _redirect_with_cookie(request, next, refreshed_session) + return _redirect_with_cookie(request, context.path, refreshed_session) @router.get("/login/oidc/start") @@ -218,7 +218,7 @@ async def login_oidc_start(request: Request, next: str = "/"): if not method_name or not method: return _render(request, "error.html", status_code=400, title="OIDC unavailable", message="The selected policy does not require OIDC.") - session_state = request.app.state.session_service.create_oidc_state(method_name, normalize_host(request.headers.get("host", "")), next) + session_state = request.app.state.session_service.create_oidc_state(method_name, normalize_host(request.headers.get("host", "")), context.path) redirect_uri = build_external_url(request, "/login/oidc/callback") return await request.app.state.oidc_service.build_authorization_redirect( request,