Scope Django Querysets to the Current User or Tenant

Flags Django views and DRF viewsets that load objects by ID without filtering on the requesting user or tenant, the root cause of IDOR bugs.

What it flags

Flaggedbilling/views.py
from rest_framework import permissions, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from .models import Invoice
from .serializers import InvoiceSerializer


class InvoiceViewSet(viewsets.ModelViewSet):
    # Any signed-in user can read or edit any invoice by changing the ID
    queryset = Invoice.objects.all()
    serializer_class = InvoiceSerializer
    permission_classes = [permissions.IsAuthenticated]

    @action(detail=True, methods=["post"])
    def void(self, request, pk=None):
        invoice = Invoice.objects.get(pk=pk)
        invoice.void(by=request.user)
        return Response({"status": "voided"})
Passesbilling/views.py
from rest_framework import permissions, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from .models import Invoice
from .serializers import InvoiceSerializer


class InvoiceViewSet(viewsets.ModelViewSet):
    serializer_class = InvoiceSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        return Invoice.objects.filter(organization=self.request.user.organization)

    def perform_create(self, serializer):
        serializer.save(organization=self.request.user.organization)

    @action(detail=True, methods=["post"])
    def void(self, request, pk=None):
        invoice = self.get_object()
        invoice.void(by=request.user)
        return Response({"status": "voided"})

Why it matters

An endpoint that runs Invoice.objects.get(pk=pk) for any signed-in user lets anyone read or change another customer's record by editing the ID in the URL, an insecure direct object reference. LoginRequiredMixin and DRF's IsAuthenticated confirm who the caller is and say nothing about which rows they may touch, so the view looks protected in review. Tests usually create one user who owns every fixture, so the missing filter never fails a test.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Scope Django Querysets to the Current User or Tenant

Applies to `**/*.py`.

Review Django views, Django REST Framework viewsets, and serializers for object access that is not scoped to the requesting user or tenant.

Flag changes that:
- Fetch a user- or tenant-owned model by ID with `Model.objects.get()`, `get_object_or_404(Model, ...)`, or `filter(pk=...)` without also filtering on `request.user` or the tenant.
- Set `model = Model` or `queryset = Model.objects.all()` on a `DetailView`, `UpdateView`, `DeleteView`, or DRF viewset for owned data without overriding `get_queryset()` to filter by the requesting user or tenant.
- Load objects directly in DRF custom actions instead of calling `self.get_object()`, which applies `get_queryset()` scoping and `has_object_permission()` checks, or override `get_object()` without calling `self.check_object_permissions()`.
- Rely on `IsAuthenticated`, `LoginRequiredMixin`, or `@login_required` as the only protection for objects that belong to a specific user or tenant.
- Accept related IDs through `PrimaryKeyRelatedField(queryset=Model.objects.all())` or a similar field, including the fields `ModelSerializer` generates for writable foreign keys, which lets a caller attach another tenant's objects.
- Update or delete with `Model.objects.filter(pk=...).update()` or `.delete()` without the ownership filter.

Do not flag:
- Models that are public by design, such as published articles or a product catalog.
- Lookups through a scoped relation or manager, such as `request.user.invoices.get(pk=pk)` or `get_object_or_404(request.user.invoices, pk=pk)`.
- Views limited to staff or superusers, such as Django admin `ModelAdmin` classes or views guarded by `is_staff` checks.

Checks files matching **/*.py.

More Django agents

See all Django code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code