Skip to content

Commit 68f2c4d

Browse files
committed
security: fix CodeQL alerts for URL sanitization and open redirect
- Fix fief-dev#1 (High): Use urlparse hostname comparison instead of string startswith() in well-known endpoint test to prevent incomplete URL substring sanitization (CWE-20) - Fix fief-dev#2 (Medium): Validate logout redirect_uri against registered client redirect URI origins to prevent open redirect attacks (CWE-601). The logout endpoint now queries tenant clients and builds an allowlist of permitted origins before redirecting. - Update logout tests to use redirect URIs matching registered client origins (https://nantes.city/)
1 parent d5e6106 commit 68f2c4d

File tree

3 files changed

+38
-5
lines changed

3 files changed

+38
-5
lines changed

fief/apps/auth/routers/auth.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import APIRouter, Depends, Query, Request, status
44
from fastapi.responses import RedirectResponse
55
from pydantic import AnyUrl
6+
from sqlalchemy import select
67

78
from fief.apps.auth.forms.auth import ConsentForm, LoginForm
89
from fief.apps.auth.forms.verify_email import VerifyEmailForm
@@ -43,6 +44,7 @@
4344
from fief.locale import gettext_lazy as _
4445
from fief.models import Client, LoginSession, OAuthProvider, Tenant, User
4546
from fief.models.session_token import SessionToken
47+
from fief.repositories import ClientRepository
4648
from fief.repositories.session_token import SessionTokenRepository
4749
from fief.schemas.auth import LogoutError
4850
from fief.services.acr import ACR
@@ -347,6 +349,9 @@ async def logout(
347349
session_token_repository: SessionTokenRepository = Depends(
348350
get_repository(SessionTokenRepository)
349351
),
352+
client_repository: ClientRepository = Depends(
353+
get_repository(ClientRepository)
354+
),
350355
tenant: Tenant = Depends(get_current_tenant),
351356
):
352357
if redirect_uri is None:
@@ -355,10 +360,35 @@ async def logout(
355360
tenant,
356361
)
357362

363+
# Validate redirect_uri against registered client redirect URIs
364+
# to prevent open redirect attacks (CWE-601)
365+
redirect_uri_str = str(redirect_uri)
366+
parsed = urllib.parse.urlparse(redirect_uri_str.replace("\\", ""))
367+
if parsed.scheme and parsed.netloc:
368+
clients = await client_repository.list(
369+
select(Client).where(Client.tenant_id == tenant.id)
370+
)
371+
allowed_origins = set()
372+
for client in clients:
373+
for uri in client.redirect_uris:
374+
client_parsed = urllib.parse.urlparse(uri)
375+
if client_parsed.scheme and client_parsed.netloc:
376+
allowed_origins.add(
377+
f"{client_parsed.scheme}://{client_parsed.netloc}"
378+
)
379+
redirect_origin = f"{parsed.scheme}://{parsed.netloc}"
380+
if redirect_origin not in allowed_origins:
381+
raise LogoutException(
382+
LogoutError.get_invalid_request(
383+
_("redirect_uri is not allowed for this tenant")
384+
),
385+
tenant,
386+
)
387+
358388
if session_token is not None:
359389
await session_token_repository.delete(session_token)
360390

361-
response = RedirectResponse(str(redirect_uri), status_code=status.HTTP_302_FOUND)
391+
response = RedirectResponse(redirect_uri_str, status_code=status.HTTP_302_FOUND)
362392

363393
response.delete_cookie(
364394
settings.session_cookie_name,

tests/test_apps_auth_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,7 @@ async def test_no_session_token(
14101410
if cookie is not None:
14111411
cookies[settings.session_cookie_name] = cookie
14121412

1413-
redirect_uri = "https://www.bretagne.duchy/"
1413+
redirect_uri = "https://nantes.city/"
14141414
response = await test_client_auth.get(
14151415
f"{path_prefix}/logout",
14161416
params={"redirect_uri": redirect_uri},
@@ -1435,7 +1435,7 @@ async def test_valid_session_token(
14351435
cookies = {}
14361436
cookies[settings.session_cookie_name] = session_token_tokens["regular"][0]
14371437

1438-
redirect_uri = "https://www.bretagne.duchy/"
1438+
redirect_uri = "https://nantes.city/"
14391439
response = await test_client_auth.get(
14401440
f"{path_prefix}/logout",
14411441
params={"redirect_uri": redirect_uri},

tests/test_apps_auth_well_known.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from urllib.parse import urlparse
2+
13
import httpx
24
import pytest
35
from fastapi import status
@@ -41,10 +43,11 @@ async def test_return_configuration(
4143
assert json["code_challenge_methods_supported"] == ["plain", "S256"]
4244

4345
endpoint: str = json["authorization_endpoint"]
46+
parsed = urlparse(endpoint)
4447
if x_forwarded_host is not None:
45-
assert endpoint.startswith(f"http://{x_forwarded_host}")
48+
assert parsed.hostname == x_forwarded_host
4649
else:
47-
assert endpoint.startswith("http://api.fief.dev")
50+
assert parsed.hostname == "api.fief.dev"
4851

4952

5053
@pytest.mark.asyncio

0 commit comments

Comments
 (0)