* security: fix CVE-2024-38371 (#10229) * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Co-authored-by: Jens L <jens@goauthentik.io>
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							6bb180f94e
						
					
				
				
					commit
					3a6c42fefb
				
			@ -4,9 +4,10 @@ from urllib.parse import urlencode
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import Application
 | 
					from authentik.core.models import Application, Group
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
 | 
				
			||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
 | 
					from authentik.policies.models import PolicyBinding
 | 
				
			||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
 | 
					from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
 | 
				
			||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
 | 
					from authentik.providers.oauth2.tests.utils import OAuthTestCase
 | 
				
			||||||
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
 | 
					from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
 | 
				
			||||||
@ -77,3 +78,23 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
				
			|||||||
            + "?"
 | 
					            + "?"
 | 
				
			||||||
            + urlencode({QS_KEY_CODE: token.user_code}),
 | 
					            + urlencode({QS_KEY_CODE: token.user_code}),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_device_init_denied(self):
 | 
				
			||||||
 | 
					        """Test device init"""
 | 
				
			||||||
 | 
					        group = Group.objects.create(name="foo")
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(
 | 
				
			||||||
 | 
					            group=group,
 | 
				
			||||||
 | 
					            target=self.application,
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        token = DeviceToken.objects.create(
 | 
				
			||||||
 | 
					            user_code="foo",
 | 
				
			||||||
 | 
					            provider=self.provider,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        res = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_providers_oauth2_root:device-login")
 | 
				
			||||||
 | 
					            + "?"
 | 
				
			||||||
 | 
					            + urlencode({QS_KEY_CODE: token.user_code})
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(res.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertIn(b"Permission denied", res.content)
 | 
				
			||||||
 | 
				
			|||||||
@ -12,10 +12,11 @@ from django.views.decorators.csrf import csrf_exempt
 | 
				
			|||||||
from rest_framework.throttling import AnonRateThrottle
 | 
					from rest_framework.throttling import AnonRateThrottle
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import Application
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.lib.utils.time import timedelta_from_string
 | 
					from authentik.lib.utils.time import timedelta_from_string
 | 
				
			||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
 | 
					from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
 | 
				
			||||||
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
 | 
					from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -38,7 +39,9 @@ class DeviceView(View):
 | 
				
			|||||||
        ).first()
 | 
					        ).first()
 | 
				
			||||||
        if not provider:
 | 
					        if not provider:
 | 
				
			||||||
            return HttpResponseBadRequest()
 | 
					            return HttpResponseBadRequest()
 | 
				
			||||||
        if not get_application(provider):
 | 
					        try:
 | 
				
			||||||
 | 
					            _ = provider.application
 | 
				
			||||||
 | 
					        except Application.DoesNotExist:
 | 
				
			||||||
            return HttpResponseBadRequest()
 | 
					            return HttpResponseBadRequest()
 | 
				
			||||||
        self.provider = provider
 | 
					        self.provider = provider
 | 
				
			||||||
        self.client_id = client_id
 | 
					        self.client_id = client_id
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,10 @@
 | 
				
			|||||||
"""Device flow views"""
 | 
					"""Device flow views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Any, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from django.views import View
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
from rest_framework.exceptions import ErrorDetail
 | 
					 | 
				
			||||||
from rest_framework.fields import CharField, IntegerField
 | 
					from rest_framework.fields import CharField, IntegerField
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,6 +17,7 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
 | 
				
			|||||||
from authentik.flows.stage import ChallengeStageView
 | 
					from authentik.flows.stage import ChallengeStageView
 | 
				
			||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.lib.utils.urls import redirect_with_qs
 | 
					from authentik.lib.utils.urls import redirect_with_qs
 | 
				
			||||||
 | 
					from authentik.policies.views import PolicyAccessView
 | 
				
			||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
 | 
					from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
 | 
				
			||||||
from authentik.providers.oauth2.views.device_finish import (
 | 
					from authentik.providers.oauth2.views.device_finish import (
 | 
				
			||||||
    PLAN_CONTEXT_DEVICE,
 | 
					    PLAN_CONTEXT_DEVICE,
 | 
				
			||||||
@ -44,48 +44,52 @@ def get_application(provider: OAuth2Provider) -> Optional[Application]:
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
 | 
					class CodeValidatorView(PolicyAccessView):
 | 
				
			||||||
    """Validate user token"""
 | 
					    """Helper to validate frontside token"""
 | 
				
			||||||
    token = DeviceToken.objects.filter(
 | 
					 | 
				
			||||||
        user_code=code,
 | 
					 | 
				
			||||||
    ).first()
 | 
					 | 
				
			||||||
    if not token:
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app = get_application(token.provider)
 | 
					    def __init__(self, code: str, **kwargs: Any) -> None:
 | 
				
			||||||
    if not app:
 | 
					        super().__init__(**kwargs)
 | 
				
			||||||
        return None
 | 
					        self.code = code
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
 | 
					    def resolve_provider_application(self):
 | 
				
			||||||
    planner = FlowPlanner(token.provider.authorization_flow)
 | 
					        self.token = DeviceToken.objects.filter(user_code=self.code).first()
 | 
				
			||||||
    planner.allow_empty_flows = True
 | 
					        if not self.token:
 | 
				
			||||||
    try:
 | 
					            raise Application.DoesNotExist
 | 
				
			||||||
        plan = planner.plan(
 | 
					        self.provider = self.token.provider
 | 
				
			||||||
            request,
 | 
					        self.application = self.token.provider.application
 | 
				
			||||||
            {
 | 
					
 | 
				
			||||||
                PLAN_CONTEXT_SSO: True,
 | 
					    def get(self, request: HttpRequest, *args, **kwargs):
 | 
				
			||||||
                PLAN_CONTEXT_APPLICATION: app,
 | 
					        scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
 | 
				
			||||||
                # OAuth2 related params
 | 
					        planner = FlowPlanner(self.provider.authorization_flow)
 | 
				
			||||||
                PLAN_CONTEXT_DEVICE: token,
 | 
					        planner.allow_empty_flows = True
 | 
				
			||||||
                # Consent related params
 | 
					        planner.use_cache = False
 | 
				
			||||||
                PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
 | 
					        try:
 | 
				
			||||||
                % {"application": app.name},
 | 
					            plan = planner.plan(
 | 
				
			||||||
                PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
 | 
					                request,
 | 
				
			||||||
            },
 | 
					                {
 | 
				
			||||||
 | 
					                    PLAN_CONTEXT_SSO: True,
 | 
				
			||||||
 | 
					                    PLAN_CONTEXT_APPLICATION: self.application,
 | 
				
			||||||
 | 
					                    # OAuth2 related params
 | 
				
			||||||
 | 
					                    PLAN_CONTEXT_DEVICE: self.token,
 | 
				
			||||||
 | 
					                    # Consent related params
 | 
				
			||||||
 | 
					                    PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
 | 
				
			||||||
 | 
					                    % {"application": self.application.name},
 | 
				
			||||||
 | 
					                    PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        except FlowNonApplicableException:
 | 
				
			||||||
 | 
					            LOGGER.warning("Flow not applicable to user")
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
 | 
				
			||||||
 | 
					        request.session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        return redirect_with_qs(
 | 
				
			||||||
 | 
					            "authentik_core:if-flow",
 | 
				
			||||||
 | 
					            request.GET,
 | 
				
			||||||
 | 
					            flow_slug=self.token.provider.authorization_flow.slug,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    except FlowNonApplicableException:
 | 
					 | 
				
			||||||
        LOGGER.warning("Flow not applicable to user")
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
    plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
 | 
					 | 
				
			||||||
    request.session[SESSION_KEY_PLAN] = plan
 | 
					 | 
				
			||||||
    return redirect_with_qs(
 | 
					 | 
				
			||||||
        "authentik_core:if-flow",
 | 
					 | 
				
			||||||
        request.GET,
 | 
					 | 
				
			||||||
        flow_slug=token.provider.authorization_flow.slug,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DeviceEntryView(View):
 | 
					class DeviceEntryView(PolicyAccessView):
 | 
				
			||||||
    """View used to initiate the device-code flow, url entered by endusers"""
 | 
					    """View used to initiate the device-code flow, url entered by endusers"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
					    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
@ -95,7 +99,9 @@ class DeviceEntryView(View):
 | 
				
			|||||||
            LOGGER.info("Brand has no device code flow configured", brand=brand)
 | 
					            LOGGER.info("Brand has no device code flow configured", brand=brand)
 | 
				
			||||||
            return HttpResponse(status=404)
 | 
					            return HttpResponse(status=404)
 | 
				
			||||||
        if QS_KEY_CODE in request.GET:
 | 
					        if QS_KEY_CODE in request.GET:
 | 
				
			||||||
            validation = validate_code(request.GET[QS_KEY_CODE], request)
 | 
					            validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
 | 
				
			||||||
 | 
					                request
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            if validation:
 | 
					            if validation:
 | 
				
			||||||
                return validation
 | 
					                return validation
 | 
				
			||||||
            LOGGER.info("Got code from query parameter but no matching token found")
 | 
					            LOGGER.info("Got code from query parameter but no matching token found")
 | 
				
			||||||
@ -130,6 +136,13 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
 | 
				
			|||||||
    code = IntegerField()
 | 
					    code = IntegerField()
 | 
				
			||||||
    component = CharField(default="ak-provider-oauth2-device-code")
 | 
					    component = CharField(default="ak-provider-oauth2-device-code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_code(self, code: int) -> HttpResponse | None:
 | 
				
			||||||
 | 
					        """Validate code and save the returned http response"""
 | 
				
			||||||
 | 
					        response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
 | 
				
			||||||
 | 
					        if not response:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Invalid code"), "invalid")
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OAuthDeviceCodeStage(ChallengeStageView):
 | 
					class OAuthDeviceCodeStage(ChallengeStageView):
 | 
				
			||||||
    """Flow challenge for users to enter device codes"""
 | 
					    """Flow challenge for users to enter device codes"""
 | 
				
			||||||
@ -145,12 +158,4 @@ class OAuthDeviceCodeStage(ChallengeStageView):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
 | 
					    def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
 | 
				
			||||||
        code = response.validated_data["code"]
 | 
					        return response.validated_data["code"]
 | 
				
			||||||
        validation = validate_code(code, self.request)
 | 
					 | 
				
			||||||
        if not validation:
 | 
					 | 
				
			||||||
            response._errors.setdefault("code", [])
 | 
					 | 
				
			||||||
            response._errors["code"].append(ErrorDetail(_("Invalid code"), "invalid"))
 | 
					 | 
				
			||||||
            return self.challenge_invalid(response)
 | 
					 | 
				
			||||||
        # Run cancel to cleanup the current flow
 | 
					 | 
				
			||||||
        self.executor.cancel()
 | 
					 | 
				
			||||||
        return validation
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										23
									
								
								website/docs/security/CVE-2024-38371.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								website/docs/security/CVE-2024-38371.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# CVE-2024-38371
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_Reported by Stefan Zwanenburg_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Insufficient access control for OAuth2 Device Code flow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Impact
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Due to a bug, access restrictions assigned to an application were not checked when using the OAuth2 Device code flow. This could potentially allow users without the correct authorization to get OAuth tokens for an application, and access the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Patches
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					authentik 2024.6.0, 2024.4.3 and 2024.2.4 fix this issue, for other versions the workaround can be used.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Workarounds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					As authentik flows are still used as part of the OAuth2 Device code flow, it is possible to add access control to the configured flows.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### For more information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you have any questions or comments about this advisory:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
 | 
				
			||||||
@ -410,6 +410,7 @@ const docsSidebar = {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            items: [
 | 
					            items: [
 | 
				
			||||||
                "security/policy",
 | 
					                "security/policy",
 | 
				
			||||||
 | 
					                "security/CVE-2024-38371",
 | 
				
			||||||
                "security/CVE-2024-23647",
 | 
					                "security/CVE-2024-23647",
 | 
				
			||||||
                "security/CVE-2024-21637",
 | 
					                "security/CVE-2024-21637",
 | 
				
			||||||
                "security/CVE-2023-48228",
 | 
					                "security/CVE-2023-48228",
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user