Opened 2 hours ago

Last modified 2 hours ago

#37213 new Bug

`ForeignKeyRawIdWidget` renders labels for related objects outside the form field's queryset

Reported by: Natalia Bidart Owned by:
Component: contrib.admin Version: 6.0
Severity: Normal Keywords: ForeignKeyRawIdWidget formfield_for_foreignkey not-security
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The Security Team received this report and concluded this is worth a public hardening:

ForeignKeyRawIdWidget.label_and_url_for_value() resolves the submitted value through the related model's default manager:

    self.rel.model._default_manager.using(db).get(**{key: value})

This ignores the queryset assigned to the bound form field. When a ModelAdmin narrows a raw-id foreign key via formfield_for_foreignkey() (for example, restricting the queryset per request), the widget still renders the __str__() label and admin change link for any valid primary key, including values outside that narrowed queryset.

The admin add view seeds initial form data from request.GET, so a staff user with add permission on the referencing model can pass an arbitrary related-object primary key in the query string and see the rendered label and change URL, even for objects the field's queryset would reject on submit. Form validation still rejects the value on POST, and the related model's admin views still enforce their own permissions (the change view returns 403), so this is limited to the label and URL rendered at GET time.

The widget's display path should be consistent with the field's queryset: resolve the value through the bound field's queryset rather than the model's default manager, and omit the label and link when the value falls outside it (matching the existing behavior for a missing primary key).

Thanks Jaeyoung Jang for the report.

Change History (1)

comment:1 by Natalia Bidart, 2 hours ago

To reproduce, using these from the admin tests:

# Models

from django.contrib.auth.models import User
from django.db import models


class Car(models.Model):
    owner = models.ForeignKey(User, models.CASCADE)
    make = models.CharField(max_length=30)
    model = models.CharField(max_length=30)

    def __str__(self):
        # Stands in for a label that may hold sensitive data
        # (customer name, private project, tenant asset, etc.).
        return "%s %s" % (self.make, self.model)


class CarTire(models.Model):
    car = models.ForeignKey(Car, models.CASCADE)


# Admin

from django.contrib import admin


class CarTireAdmin(admin.ModelAdmin):
    raw_id_fields = ["car"]

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)


admin.site.register(CarTire, CarTireAdmin)

Reproduction, given a staff user Alex with only add_cartire, owning Car #1, and Robin owning Car #2:

GET /admin/admin_widgets/car/                -> 403   (no view_car)
GET /admin/admin_widgets/car/2/change/       -> 403   (no change_car)
GET /admin/admin_widgets/cartire/add/?car=2  -> 200   (renders Robin's label + change link)
POST /admin/admin_widgets/cartire/add/  car=2 -> rejected by validation, no CarTire created
Last edited 2 hours ago by Natalia Bidart (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top