From 65c332a70a37198aea928e148bf5ebf061e7e5af Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Thu, 13 Feb 2025 21:12:36 +0100 Subject: [PATCH] Add two checks to prevent security issues with GraphQL mutations/queries --- aleksis/core/checks.py | 29 ++++++++++++++++++++++++++++- aleksis/core/schema/__init__.py | 12 ++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/aleksis/core/checks.py b/aleksis/core/checks.py index 68ccb06ba..d206c808d 100644 --- a/aleksis/core/checks.py +++ b/aleksis/core/checks.py @@ -1,9 +1,11 @@ +from collections.abc import Iterable from typing import Optional import django.apps -from django.core.checks import Tags, Warning, register # noqa +from django.core.checks import Error, Tags, Warning, register # noqa from .mixins import ExtensibleModel, GlobalPermissionModel, PureDjangoModel +from .schema.base import BaseBatchCreateMutation, BaseBatchDeleteMutation, BaseBatchPatchMutation from .util.apps import AppConfig @@ -71,3 +73,28 @@ def check_app_models_base_class( ) return results + + +@register(Tags.security) +def check_all_mutations_with_permissions( + app_configs: Optional[django.apps.registry.Apps] = None, **kwargs +) -> list: + results = [] + for base_class in [BaseBatchCreateMutation, BaseBatchPatchMutation, BaseBatchDeleteMutation]: + for subclass in base_class.__subclasses__(): + if ( + not isinstance(subclass._meta.permissions, Iterable) + or not subclass._meta.permissions + ): + results.append( + Error( + f"Mutation {subclass.__name__} doesn't set required permission", + hint=( + "Ensure that the mutation is protected by setting the " + "permissions attribute in the mutation's Meta class." + ), + obj=subclass, + id="aleksis.core.E001", + ) + ) + return results diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 696e2867e..97de056ca 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -4,6 +4,7 @@ from django.db.models import Q import graphene import graphene_django_optimizer +from graphene.types.resolver import dict_or_attr_resolver, set_default_resolver from guardian.shortcuts import get_objects_for_user from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet @@ -81,6 +82,17 @@ from .two_factor import TwoFactorType from .user import UserType +def custom_default_resolver(attname, default_value, root, info, **args): + """Custom default resolver to ensure resolvers are set for all queries.""" + if info.parent_type.name == "GlobalQuery": + raise NotImplementedError(f"No own resolver defined for {attname}") + + return dict_or_attr_resolver(attname, default_value, root, info, **args) + + +set_default_resolver(custom_default_resolver) + + class Query(graphene.ObjectType): ping = graphene.String(payload=graphene.String()) -- GitLab