From e10c47d8b88a99f593754cfddeb8c5e65f6865e8 Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:24:31 +0100 Subject: [PATCH] security: fix CVE 2024 52287 (cherry-pick #12114) (#12117) security: fix CVE 2024 52287 (#12114) * security: CVE-2024-52287 * add tests --------- Signed-off-by: Jens Langhammer Co-authored-by: Jens L. --- .../oauth2/tests/test_token_cc_standard.py | 44 ++++++++++++++++++- .../oauth2/tests/test_token_device.py | 33 +++++++++++++- authentik/providers/oauth2/views/token.py | 24 ++++++++-- website/docs/security/cves/CVE-2024-52287.md | 27 ++++++++++++ website/sidebars.js | 1 + 5 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 website/docs/security/cves/CVE-2024-52287.md diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard.py b/authentik/providers/oauth2/tests/test_token_cc_standard.py index 7b233794cd..a0abf3b7a6 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_standard.py +++ b/authentik/providers/oauth2/tests/test_token_cc_standard.py @@ -19,7 +19,7 @@ from authentik.providers.oauth2.constants import ( TOKEN_TYPE, ) from authentik.providers.oauth2.errors import TokenError -from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping +from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping from authentik.providers.oauth2.tests.utils import OAuthTestCase @@ -107,6 +107,48 @@ class TestTokenClientCredentialsStandard(OAuthTestCase): {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, ) + def test_incorrect_scopes(self): + """test scope that isn't configured""" + response = self.client.post( + reverse("authentik_providers_oauth2:token"), + { + "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE} extra_scope", + "client_id": self.provider.client_id, + "client_secret": self.provider.client_secret, + }, + ) + self.assertEqual(response.status_code, 200) + body = loads(response.content.decode()) + self.assertEqual(body["token_type"], TOKEN_TYPE) + token = AccessToken.objects.filter( + provider=self.provider, token=body["access_token"] + ).first() + self.assertSetEqual( + set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE} + ) + _, alg = self.provider.jwt_key + jwt = decode( + body["access_token"], + key=self.provider.signing_key.public_key, + algorithms=[alg], + audience=self.provider.client_id, + ) + self.assertEqual( + jwt["given_name"], "Autogenerated user from application test (client credentials)" + ) + self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") + jwt = decode( + body["id_token"], + key=self.provider.signing_key.public_key, + algorithms=[alg], + audience=self.provider.client_id, + ) + self.assertEqual( + jwt["given_name"], "Autogenerated user from application test (client credentials)" + ) + self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") + def test_successful(self): """test successful""" response = self.client.post( diff --git a/authentik/providers/oauth2/tests/test_token_device.py b/authentik/providers/oauth2/tests/test_token_device.py index 308b8d2d28..b1b7aef5f3 100644 --- a/authentik/providers/oauth2/tests/test_token_device.py +++ b/authentik/providers/oauth2/tests/test_token_device.py @@ -9,8 +9,12 @@ from authentik.blueprints.tests import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.lib.generators import generate_code_fixed_length, generate_id -from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE -from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping +from authentik.providers.oauth2.constants import ( + GRANT_TYPE_DEVICE_CODE, + SCOPE_OPENID, + SCOPE_OPENID_EMAIL, +) +from authentik.providers.oauth2.models import AccessToken, DeviceToken, OAuth2Provider, ScopeMapping from authentik.providers.oauth2.tests.utils import OAuthTestCase @@ -80,3 +84,28 @@ class TestTokenDeviceCode(OAuthTestCase): }, ) self.assertEqual(res.status_code, 200) + + def test_code_mismatched_scope(self): + """Test code with user (mismatched scopes)""" + device_token = DeviceToken.objects.create( + provider=self.provider, + user_code=generate_code_fixed_length(), + device_code=generate_id(), + user=self.user, + scope=[SCOPE_OPENID, SCOPE_OPENID_EMAIL], + ) + res = self.client.post( + reverse("authentik_providers_oauth2:token"), + data={ + "client_id": self.provider.client_id, + "grant_type": GRANT_TYPE_DEVICE_CODE, + "device_code": device_token.device_code, + "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} invalid", + }, + ) + self.assertEqual(res.status_code, 200) + body = loads(res.content) + token = AccessToken.objects.filter( + provider=self.provider, token=body["access_token"] + ).first() + self.assertSetEqual(set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL}) diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py index cfb75fedd9..63fbf20a87 100644 --- a/authentik/providers/oauth2/views/token.py +++ b/authentik/providers/oauth2/views/token.py @@ -59,6 +59,7 @@ from authentik.providers.oauth2.models import ( DeviceToken, OAuth2Provider, RefreshToken, + ScopeMapping, ) from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES @@ -77,7 +78,7 @@ class TokenParams: redirect_uri: str grant_type: str state: str - scope: list[str] + scope: set[str] provider: OAuth2Provider @@ -112,11 +113,26 @@ class TokenParams: redirect_uri=request.POST.get("redirect_uri", ""), grant_type=request.POST.get("grant_type", ""), state=request.POST.get("state", ""), - scope=request.POST.get("scope", "").split(), + scope=set(request.POST.get("scope", "").split()), # PKCE parameter. code_verifier=request.POST.get("code_verifier"), ) + def __check_scopes(self): + allowed_scope_names = set( + ScopeMapping.objects.filter(provider__in=[self.provider]).values_list( + "scope_name", flat=True + ) + ) + scopes_to_check = self.scope + if not scopes_to_check.issubset(allowed_scope_names): + LOGGER.info( + "Application requested scopes not configured, setting to overlap", + scope_allowed=allowed_scope_names, + scope_given=self.scope, + ) + self.scope = self.scope.intersection(allowed_scope_names) + def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs): with start_span( op="authentik.providers.oauth2.token.policy", @@ -149,7 +165,7 @@ class TokenParams: client_id=self.provider.client_id, ) raise TokenError("invalid_client") - + self.__check_scopes() if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: with start_span( op="authentik.providers.oauth2.post.parse.code", @@ -710,7 +726,7 @@ class TokenView(View): "id_token": access_token.id_token.to_jwt(self.provider), } - if SCOPE_OFFLINE_ACCESS in self.params.scope: + if SCOPE_OFFLINE_ACCESS in self.params.device_code.scope: refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity) refresh_token = RefreshToken( user=self.params.device_code.user, diff --git a/website/docs/security/cves/CVE-2024-52287.md b/website/docs/security/cves/CVE-2024-52287.md new file mode 100644 index 0000000000..05852e9f76 --- /dev/null +++ b/website/docs/security/cves/CVE-2024-52287.md @@ -0,0 +1,27 @@ +# CVE-2024-52287 + +_Reported by [@matt1097](https://github.com/matt1097)_ + +## Insufficient validation of OAuth scopes for client_credentials and device_code grants + +### Summary + +When using the `client_credentials` or `device_code` OAuth grants, it was possible for an attacker to get a token from authentik with scopes that haven't been configured in authentik. + +### Details + +With the `device_code` grant, it was possible to have a user authorize a set of permitted scopes, and then acquire a token with a different set of scopes, including scopes not configured. This token could potentially be used to send requests to another system which trusts tokens signed by authentik and execute malicious actions on behalf of the user. + +With the `client_credentials` grant, because there is no user authorization process, authentik would not validate the scopes requested for the token, allowing tokens to be issued with scopes not configured in authentik. These could similarly be used to execute malicious actions in other systems. + +There is no workaround for this issue; however this issue could only be exploited if an attacker possesses a valid set of OAuth2 `client_id` and `client_secret` credentials, and has the knowledge of another system that trusts tokens issued by authentik and what scopes it checks for. + +### Patches + +authentik 2024.8.5 and 2024.10.3 fix this issue. + +### 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 dc3aadf708..0e35360e19 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -656,6 +656,7 @@ export default { type: "category", label: "2024", items: [ + "security/cves/CVE-2024-52287", "security/cves/CVE-2024-47077", "security/cves/CVE-2024-47070", "security/cves/CVE-2024-38371",