diff --git a/authentik/providers/oauth2/tests/test_authorize.py b/authentik/providers/oauth2/tests/test_authorize.py index 7903c685c..cc82c3b9c 100644 --- a/authentik/providers/oauth2/tests/test_authorize.py +++ b/authentik/providers/oauth2/tests/test_authorize.py @@ -87,6 +87,25 @@ class TestAuthorize(OAuthTestCase): ) OAuthAuthorizationParams.from_request(request) + def test_blocked_redirect_uri(self): + """test missing/invalid redirect URI""" + OAuth2Provider.objects.create( + name=generate_id(), + client_id="test", + authorization_flow=create_test_flow(), + redirect_uris="data:local.invalid", + ) + with self.assertRaises(RedirectUriError): + request = self.factory.get( + "/", + data={ + "response_type": "code", + "client_id": "test", + "redirect_uri": "data:localhost", + }, + ) + OAuthAuthorizationParams.from_request(request) + def test_invalid_redirect_uri_empty(self): """test missing/invalid redirect URI""" provider = OAuth2Provider.objects.create( diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py index 7ad76a642..0baa942b4 100644 --- a/authentik/providers/oauth2/views/authorize.py +++ b/authentik/providers/oauth2/views/authorize.py @@ -76,6 +76,7 @@ PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params" SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid" ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN} +FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"} @dataclass(slots=True) @@ -179,6 +180,10 @@ class OAuthAuthorizationParams: self.check_scope(github_compat) self.check_nonce() self.check_code_challenge() + if self.request: + raise AuthorizeError( + self.redirect_uri, "request_not_supported", self.grant_type, self.state + ) def check_redirect_uri(self): """Redirect URI validation.""" @@ -216,10 +221,9 @@ class OAuthAuthorizationParams: redirect_uri_expected=allowed_redirect_urls, ) raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) - if self.request: - raise AuthorizeError( - self.redirect_uri, "request_not_supported", self.grant_type, self.state - ) + # Check against forbidden schemes + if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: + raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) def check_scope(self, github_compat=False): """Ensure openid scope is set in Hybrid flows, or when requesting an id_token""" diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py index 3bf134b09..4168edb1e 100644 --- a/authentik/providers/oauth2/views/token.py +++ b/authentik/providers/oauth2/views/token.py @@ -6,6 +6,7 @@ from hashlib import sha256 from re import error as RegexError from re import fullmatch from typing import Any, Optional +from urllib.parse import urlparse from django.http import HttpRequest, HttpResponse from django.utils import timezone @@ -55,6 +56,7 @@ from authentik.providers.oauth2.models import ( RefreshToken, ) from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth +from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES from authentik.sources.oauth.models import OAuthSource from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS @@ -206,6 +208,10 @@ class TokenParams: ).from_http(request) raise TokenError("invalid_client") + # Check against forbidden schemes + if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: + raise TokenError("invalid_request") + self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first() if not self.authorization_code: LOGGER.warning("Code does not exist", code=raw_code) diff --git a/website/docs/security/CVE-2024-21637.md b/website/docs/security/CVE-2024-21637.md new file mode 100644 index 000000000..f7322ed2e --- /dev/null +++ b/website/docs/security/CVE-2024-21637.md @@ -0,0 +1,39 @@ +# CVE-2024-21637 + +_Reported by [@lauritzh](https://github.com/lauritzh)_ + +## XSS in Authentik via JavaScript-URI as Redirect URI and form_post Response Mode + +### Summary + +Given an OAuth2 provider configured with allowed redirect URIs set to `*` or `.*`, an attacker can send an OAuth Authorization request using `response_mode=form_post` and setting `redirect_uri` to a malicious URI, to capture authentik's session token. + +### Patches + +authentik 2023.8.6 and 2023.10.6 fix this issue. + +### Impact + +The impact depends on the attack scenario. In the following I will describe the two scenario that were identified for Authentik. + +#### Redirect URI Misconfiguration + +While advising that this may cause security issues, Authentik generally allows wildcards as Redirect URI. Therefore, using a wildcard-only effectively allowing arbitrary URLS is possible misconfiguration that may be present in real-world instances. + +In such cases, unauthenticated and unprivileged attackers can perform the above described actions. + +### User with (only) App Administration Permissions + +A more likely scenario is an administrative user (e.g. a normal developer) having only permissions to manage applications. + +This relatively user could use the described attacks to perform a privilege escalation. + +### Workaround + +It is recommended to upgrade to the patched version of authentik. If not possible, ensure that OAuth2 providers do not use a wildcard (`*` or `.*`) value as allowed redirect URI setting. (This is _not_ exploitable if part of the redirect URI has a wildcard, for example `https://foo-.*\.bar\.com`) + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io) diff --git a/website/sidebars.js b/website/sidebars.js index 25c08ea1b..764944508 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -408,6 +408,7 @@ const docsSidebar = { }, items: [ "security/policy", + "security/CVE-2024-21637", "security/CVE-2023-48228", "security/GHSA-rjvp-29xq-f62w", "security/CVE-2023-39522",