Skip to content

Security: Cross-Tenant IDOR in TenantDependentModel Mutations #688

@lighthousekeeper1212

Description

@lighthousekeeper1212

Summary

The UpdateTenantDependentModelMutation and DeleteTenantDependentModelMutation base classes accept a tenant_id parameter but never use it to scope database queries, allowing cross-tenant data modification/deletion (CWE-639).

Vulnerability Details

Root Cause: packages/backend/common/graphql/mutations.py, line 366

@classmethod
def get_queryset(cls, model_class, root, info, **input):
    return model_class.objects.all()  # Returns ALL objects from ALL tenants

UpdateTenantDependentModelMutation (lines 654-673) converts tenant_id from global ID to local ID but never passes it to the queryset:

class UpdateTenantDependentModelMutation(UpdateModelMutation):
    @classmethod
    def mutate_and_get_payload(cls, root, info, **input):
        if "tenant_id" in input:
            _, input["tenant_id"] = from_global_id(input["tenant_id"])
        return super().mutate_and_get_payload(root, info, **input)

The parent's get_object() calls get_queryset() which returns model_class.objects.all() — finds ANY object by ID regardless of tenant.

Secure pattern comparison — Query resolvers in apps/demo/schema.py correctly scope by tenant:

@permission_classes(IsTenantMemberAccess)
def resolve_crud_demo_item(root, info, id, tenant_id, **kwargs):
    _, pk = from_global_id(id)
    _, tenant_pk = from_global_id(tenant_id)
    return get_object_or_404(models.CrudDemoItem, pk=pk, tenant=tenant_pk)  # Scoped!

The @permission_classes(IsTenantMemberAccess) on TenantMemberMutation validates the user is a member of the claimed tenant, but since get_queryset() returns ALL objects, a user can pass their OWN tenant_id (passes permission check) with a target resource's id from another tenant (not scoped).

Impact

As a SaaS boilerplate, this pattern is likely copied into production applications. Affects UpdateCrudDemoItemMutation, DeleteCrudDemoItemMutation, and any app using these base classes.

Recommended Fix

Override get_queryset() in tenant-dependent base classes to filter by tenant_id.

Disclosure

Found during security research. Happy to provide additional details.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions