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 3158420c41..01b0c41cbf 100644 --- a/authentik/core/api/used_by.py +++ b/authentik/core/api/used_by.py @@ -14,6 +14,7 @@ from rest_framework.request import Request 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 95f4513f61..5bd2665347 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -35,6 +35,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg 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 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): ], 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 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): ], 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 ae3a842609..e2dc755e7c 100644 --- a/authentik/crypto/tests.py +++ b/authentik/crypto/tests.py @@ -214,6 +214,46 @@ class TestCrypto(APITestCase): 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 @@ class TestCrypto(APITestCase): ], ) + 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 767ceea309..70bee5674c 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -37,6 +37,7 @@ from authentik.lib.utils.file import ( ) 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 @@ class FlowViewSet(UsedByMixin, ModelViewSet): 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 a677ccb5a4..85dadb515c 100644 --- a/authentik/outposts/api/service_connections.py +++ b/authentik/outposts/api/service_connections.py @@ -26,6 +26,7 @@ from authentik.outposts.models import ( 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 0000000000..3a024aa80d --- /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 363b10f589..864c933158 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",