[Django] #37213: `ForeignKeyRawIdWidget` renders labels for related objects outside the form field's queryset

0 views
Skip to first unread message

Django

unread,
3:45 PM (5 hours ago) 3:45 PM
to django-...@googlegroups.com
#37213: `ForeignKeyRawIdWidget` renders labels for related objects outside the form
field's queryset
-------------------------------------+-------------------------------------
Reporter: Natalia Bidart | Type: Bug
Status: new | Component:
| contrib.admin
Version: 6.0 | Severity: Normal
Keywords: | Triage Stage:
ForeignKeyRawIdWidget | Unreviewed
formfield_for_foreignkey not- |
security |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
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:

{{{#!python
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.
--
Ticket URL: <https://code.djangoproject.com/ticket/37213>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
3:48 PM (5 hours ago) 3:48 PM
to django-...@googlegroups.com
#37213: `ForeignKeyRawIdWidget` renders labels for related objects outside the form
field's queryset
-------------------------------------+-------------------------------------
Reporter: Natalia Bidart | Owner: (none)
Type: Bug | Status: new
Component: contrib.admin | Version: 6.0
Severity: Normal | Resolution:
Keywords: | Triage Stage:
ForeignKeyRawIdWidget | Unreviewed
formfield_for_foreignkey not- |
security |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Natalia Bidart):

To reproduce:
{{{#!python
# 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
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/37213#comment:1>
Reply all
Reply to author
Forward
0 new messages