From 359b343f51524342a5ca03828e7c975a1d654b11 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, 22 Aug 2024 17:18:58 +0200 Subject: [PATCH] security: fix CVE-2024-42490 (cherry-pick #11022) (#11025) security: fix CVE-2024-42490 (#11022) CVE-2024-42490 Signed-off-by: Jens Langhammer Co-authored-by: Jens L. --- authentik/core/api/used_by.py | 3 +- authentik/crypto/api.py | 5 +- authentik/crypto/tests.py | 60 +++++++++++++++++++ authentik/flows/api/flows.py | 3 +- authentik/outposts/api/service_connections.py | 3 +- website/docs/security/CVE-2024-42490.md | 31 ++++++++++ website/sidebars.js | 1 + 7 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 website/docs/security/CVE-2024-42490.md diff --git a/authentik/core/api/used_by.py b/authentik/core/api/used_by.py index 3158420c4144..01b0c41cbf6a 100644 --- a/authentik/core/api/used_by.py +++ b/authentik/core/api/used_by.py @@ -14,6 +14,7 @@ from rest_framework.response import Response from authentik.core.api.utils import PassiveSerializer +from authentik.rbac.filters import ObjectFilter class DeleteAction(Enum): @@ -53,7 +54,7 @@ class UsedByMixin: @extend_schema( responses={200: UsedBySerializer(many=True)}, ) - @action(detail=True, pagination_class=None, filter_backends=[]) + @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) def used_by(self, request: Request, *args, **kwargs) -> Response: """Get a list of all objects that use this object""" model: Model = self.get_object() diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index 95f4513f6117..5bd2665347e7 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -35,6 +35,7 @@ from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction from authentik.rbac.decorators import permission_required +from authentik.rbac.filters import ObjectFilter LOGGER = get_logger() @@ -265,7 +266,7 @@ def generate(self, request: Request) -> Response: ], responses={200: CertificateDataSerializer(many=False)}, ) - @action(detail=True, pagination_class=None, filter_backends=[]) + @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) def view_certificate(self, request: Request, pk: str) -> Response: """Return certificate-key pairs certificate and log access""" certificate: CertificateKeyPair = self.get_object() @@ -295,7 +296,7 @@ def view_certificate(self, request: Request, pk: str) -> Response: ], responses={200: CertificateDataSerializer(many=False)}, ) - @action(detail=True, pagination_class=None, filter_backends=[]) + @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) def view_private_key(self, request: Request, pk: str) -> Response: """Return certificate-key pairs private key and log access""" certificate: CertificateKeyPair = self.get_object() diff --git a/authentik/crypto/tests.py b/authentik/crypto/tests.py index ae3a84260907..e2dc755e7ca0 100644 --- a/authentik/crypto/tests.py +++ b/authentik/crypto/tests.py @@ -214,6 +214,46 @@ def test_private_key_download(self): self.assertEqual(200, response.status_code) self.assertIn("Content-Disposition", response) + def test_certificate_download_denied(self): + """Test certificate export (download)""" + self.client.logout() + keypair = create_test_cert() + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-certificate", + kwargs={"pk": keypair.pk}, + ) + ) + self.assertEqual(403, response.status_code) + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-certificate", + kwargs={"pk": keypair.pk}, + ), + data={"download": True}, + ) + self.assertEqual(403, response.status_code) + + def test_private_key_download_denied(self): + """Test private_key export (download)""" + self.client.logout() + keypair = create_test_cert() + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-private-key", + kwargs={"pk": keypair.pk}, + ) + ) + self.assertEqual(403, response.status_code) + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-view-private-key", + kwargs={"pk": keypair.pk}, + ), + data={"download": True}, + ) + self.assertEqual(403, response.status_code) + def test_used_by(self): """Test used_by endpoint""" self.client.force_login(create_test_admin_user()) @@ -246,6 +286,26 @@ def test_used_by(self): ], ) + def test_used_by_denied(self): + """Test used_by endpoint""" + self.client.logout() + keypair = create_test_cert() + OAuth2Provider.objects.create( + name=generate_id(), + client_id="test", + client_secret=generate_key(), + authorization_flow=create_test_flow(), + redirect_uris="http://localhost", + signing_key=keypair, + ) + response = self.client.get( + reverse( + "authentik_api:certificatekeypair-used-by", + kwargs={"pk": keypair.pk}, + ) + ) + self.assertEqual(403, response.status_code) + def test_discovery(self): """Test certificate discovery""" name = generate_id() diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index 767ceea309d7..70bee5674ccb 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -37,6 +37,7 @@ ) from authentik.lib.views import bad_request_message from authentik.rbac.decorators import permission_required +from authentik.rbac.filters import ObjectFilter LOGGER = get_logger() @@ -281,7 +282,7 @@ def set_background_url(self, request: Request, slug: str): 400: OpenApiResponse(description="Flow not applicable"), }, ) - @action(detail=True, pagination_class=None, filter_backends=[]) + @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) def execute(self, request: Request, slug: str): """Execute flow for current user""" # Because we pre-plan the flow here, and not in the planner, we need to manually clear diff --git a/authentik/outposts/api/service_connections.py b/authentik/outposts/api/service_connections.py index a677ccb5a463..85dadb515c14 100644 --- a/authentik/outposts/api/service_connections.py +++ b/authentik/outposts/api/service_connections.py @@ -26,6 +26,7 @@ KubernetesServiceConnection, OutpostServiceConnection, ) +from authentik.rbac.filters import ObjectFilter class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer): @@ -75,7 +76,7 @@ class ServiceConnectionViewSet( filterset_fields = ["name"] @extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)}) - @action(detail=True, pagination_class=None, filter_backends=[]) + @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) def state(self, request: Request, pk: str) -> Response: """Get the service connection's state""" connection = self.get_object() diff --git a/website/docs/security/CVE-2024-42490.md b/website/docs/security/CVE-2024-42490.md new file mode 100644 index 000000000000..3a024aa80d90 --- /dev/null +++ b/website/docs/security/CVE-2024-42490.md @@ -0,0 +1,31 @@ +# CVE-2024-42490 + +_Reported by [@m2a2](https://github.com/m2a2)_ + +## Improper Authorization for Token modification + +### Summary + +Several API endpoints can be accessed by users without correct authentication/authorization. + +The main API endpoints affected by this: + +- `/api/v3/crypto/certificatekeypairs//view_certificate/` +- `/api/v3/crypto/certificatekeypairs//view_private_key/` +- `/api/v3/.../used_by/` + +Note that all of the affected API endpoints require the knowledge of the ID of an object, which especially for certificates is not accessible to an unprivileged user. Additionally the IDs for most objects are UUIDv4, meaning they are not easily guessable/enumerable. + +### Patches + +authentik 2024.4.4, 2024.6.4 and 2024.8.0 fix this issue. + +### Workarounds + +Access to the API endpoints can be blocked at a Reverse-proxy/Load balancer level to prevent this issue from being exploited. + +### 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 363b10f5890f..864c9331581d 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -511,6 +511,7 @@ const docsSidebar = { items: [ "security/security-hardening", "security/policy", + "security/CVE-2024-42490", "security/CVE-2024-38371", "security/CVE-2024-37905", "security/CVE-2024-23647",