Opened 3 weeks ago

Last modified 3 weeks ago

#37213 assigned Bug

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

Reported by: Natalia Bidart Owned by: Vismay
Component: contrib.admin Version: 6.0
Severity: Normal Keywords: ForeignKeyRawIdWidget formfield_for_foreignkey not-security
Cc: Vismay Triage Stage: Accepted
Has patch: yes 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 (6)

comment:1 by Natalia Bidart, 3 weeks 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 3 weeks ago by Natalia Bidart (previous) (diff)

comment:2 by Sarah Boyce, 3 weeks ago

Triage Stage: UnreviewedAccepted

comment:3 by Pravin MD, 3 weeks ago

I could reproduce this

comment:4 by Vismay, 3 weeks ago

Owner: set to Vismay
Status: newassigned

comment:5 by Vismay, 3 weeks ago

Cc: Vismay added
Has patch: set

comment:6 by Vismay, 3 weeks ago

Opened PR: https://github.com/django/django/pull/21612
I reproduced the issue locally and added a regression test in admin_widgets (it fails without the fix). The PR makes ForeignKeyRawIdWidget resolve the label and change link through the form field's queryset instead of the model's default manager, so values outside a narrowed queryset (e.g. set via formfield_for_foreignkey()) are no longer exposed.

Note: See TracTickets for help on using tickets.
Back to Top