Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2b31342

Browse files
charettestimgraham
authored andcommitted
[1.7.x] Prevented data leakage in contrib.admin via query string manipulation.
This is a security fix. Disclosure following shortly.
1 parent 1a45d05 commit 2b31342

File tree

8 files changed

+115
-7
lines changed

8 files changed

+115
-7
lines changed

django/contrib/admin/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@
44
class DisallowedModelAdminLookup(SuspiciousOperation):
55
"""Invalid filter was passed to admin view via URL querystring"""
66
pass
7+
8+
9+
class DisallowedModelAdminToField(SuspiciousOperation):
10+
"""Invalid to_field was passed to admin view via URL query string"""
11+
pass

django/contrib/admin/options.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.contrib.admin import validation
1212
from django.contrib.admin.checks import (BaseModelAdminChecks, ModelAdminChecks,
1313
InlineModelAdminChecks)
14+
from django.contrib.admin.exceptions import DisallowedModelAdminToField
1415
from django.contrib.admin.utils import (quote, unquote, flatten_fieldsets,
1516
get_deleted_objects, model_format_dict, NestedObjects,
1617
lookup_needs_distinct)
@@ -434,6 +435,24 @@ def lookup_allowed(self, lookup, value):
434435
valid_lookups.append(filter_item)
435436
return clean_lookup in valid_lookups
436437

438+
def to_field_allowed(self, request, to_field):
439+
opts = self.model._meta
440+
441+
try:
442+
field = opts.get_field(to_field)
443+
except FieldDoesNotExist:
444+
return False
445+
446+
# Make sure at least one of the models registered for this site
447+
# references this field.
448+
registered_models = self.admin_site._registry
449+
for related_object in opts.get_all_related_objects():
450+
if (related_object.model in registered_models and
451+
field in related_object.field.foreign_related_fields):
452+
return True
453+
454+
return False
455+
437456
def has_add_permission(self, request):
438457
"""
439458
Returns True if the given request has permission to add an object.
@@ -1325,6 +1344,10 @@ def get_changeform_initial_data(self, request):
13251344
@transaction.atomic
13261345
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
13271346

1347+
to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
1348+
if to_field and not self.to_field_allowed(request, to_field):
1349+
raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
1350+
13281351
model = self.model
13291352
opts = model._meta
13301353
add = object_id is None
@@ -1397,8 +1420,7 @@ def changeform_view(self, request, object_id=None, form_url='', extra_context=No
13971420
original=obj,
13981421
is_popup=(IS_POPUP_VAR in request.POST or
13991422
IS_POPUP_VAR in request.GET),
1400-
to_field=request.POST.get(TO_FIELD_VAR,
1401-
request.GET.get(TO_FIELD_VAR)),
1423+
to_field=to_field,
14021424
media=media,
14031425
inline_admin_formsets=inline_formsets,
14041426
errors=helpers.AdminErrorList(form, formsets),

django/contrib/admin/views/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from django.utils.http import urlencode
1515

1616
from django.contrib.admin import FieldListFilter
17-
from django.contrib.admin.exceptions import DisallowedModelAdminLookup
17+
from django.contrib.admin.exceptions import (
18+
DisallowedModelAdminLookup, DisallowedModelAdminToField,
19+
)
1820
from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, TO_FIELD_VAR
1921
from django.contrib.admin.utils import (quote, get_fields_from_path,
2022
lookup_needs_distinct, prepare_lookup_value)
@@ -89,7 +91,10 @@ def __init__(self, request, model, list_display, list_display_links,
8991
self.page_num = 0
9092
self.show_all = ALL_VAR in request.GET
9193
self.is_popup = _is_changelist_popup(request)
92-
self.to_field = request.GET.get(TO_FIELD_VAR)
94+
to_field = request.GET.get(TO_FIELD_VAR)
95+
if to_field and not model_admin.to_field_allowed(request, to_field):
96+
raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
97+
self.to_field = to_field
9398
self.params = dict(request.GET.items())
9499
if PAGE_VAR in self.params:
95100
del self.params[PAGE_VAR]

docs/ref/exceptions.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ SuspiciousOperation
5656

5757
* DisallowedHost
5858
* DisallowedModelAdminLookup
59+
* DisallowedModelAdminToField
5960
* DisallowedRedirect
6061
* InvalidSessionKey
6162
* SuspiciousFileOperation

docs/releases/1.4.14.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between
4747
requests without an intervening logout could result in the prior user's session
4848
being co-opted by the subsequent user. The middleware now logs the user out on
4949
a failed login attempt.
50+
51+
Data leakage via query string manipulation in ``contrib.admin``
52+
===============================================================
53+
54+
In older versions of Django it was possible to reveal any field's data by
55+
modifying the "popup" and "to_field" parameters of the query string on an admin
56+
change form page. For example, requesting a URL like
57+
``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed
58+
viewing the password hash of each user. While the admin requires users to have
59+
permissions to view the change form pages in the first place, this could leak
60+
data if you rely on users having access to view only certain fields on a model.
61+
62+
To address the issue, an exception will now be raised if a ``to_field`` value
63+
that isn't a related field to a model that has been registered with the admin
64+
is specified.

docs/releases/1.5.9.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between
4747
requests without an intervening logout could result in the prior user's session
4848
being co-opted by the subsequent user. The middleware now logs the user out on
4949
a failed login attempt.
50+
51+
Data leakage via query string manipulation in ``contrib.admin``
52+
===============================================================
53+
54+
In older versions of Django it was possible to reveal any field's data by
55+
modifying the "popup" and "to_field" parameters of the query string on an admin
56+
change form page. For example, requesting a URL like
57+
``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed
58+
viewing the password hash of each user. While the admin requires users to have
59+
permissions to view the change form pages in the first place, this could leak
60+
data if you rely on users having access to view only certain fields on a model.
61+
62+
To address the issue, an exception will now be raised if a ``to_field`` value
63+
that isn't a related field to a model that has been registered with the admin
64+
is specified.

docs/releases/1.6.6.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,21 @@ requests without an intervening logout could result in the prior user's session
4848
being co-opted by the subsequent user. The middleware now logs the user out on
4949
a failed login attempt.
5050

51+
Data leakage via query string manipulation in ``contrib.admin``
52+
===============================================================
53+
54+
In older versions of Django it was possible to reveal any field's data by
55+
modifying the "popup" and "to_field" parameters of the query string on an admin
56+
change form page. For example, requesting a URL like
57+
``/admin/auth/user/?_popup=1&t=password`` and viewing the page's HTML allowed
58+
viewing the password hash of each user. While the admin requires users to have
59+
permissions to view the change form pages in the first place, this could leak
60+
data if you rely on users having access to view only certain fields on a model.
61+
62+
To address the issue, an exception will now be raised if a ``to_field`` value
63+
that isn't a related field to a model that has been registered with the admin
64+
is specified.
65+
5166
Bugfixes
5267
========
5368

tests/admin_views/tests.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from django.contrib.admin import ModelAdmin
1919
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
2020
from django.contrib.admin.models import LogEntry, DELETION
21+
from django.contrib.admin.options import TO_FIELD_VAR
2122
from django.contrib.admin.templatetags.admin_static import static
2223
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
2324
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
@@ -599,6 +600,36 @@ def test_disallowed_filtering(self):
599600
response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
600601
self.assertEqual(response.status_code, 200)
601602

603+
def test_disallowed_to_field(self):
604+
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
605+
response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'})
606+
self.assertEqual(response.status_code, 400)
607+
self.assertEqual(len(calls), 1)
608+
609+
# Specifying a field that is not refered by any other model registered
610+
# to this admin site should raise an exception.
611+
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
612+
response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'})
613+
self.assertEqual(response.status_code, 400)
614+
self.assertEqual(len(calls), 1)
615+
616+
# Specifying a field referenced by another model should be allowed.
617+
response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'})
618+
self.assertEqual(response.status_code, 200)
619+
620+
# We also want to prevent the add and change view from leaking a
621+
# disallowed field value.
622+
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
623+
response = self.client.post("/test_admin/admin/admin_views/section/add/", {TO_FIELD_VAR: 'name'})
624+
self.assertEqual(response.status_code, 400)
625+
self.assertEqual(len(calls), 1)
626+
627+
section = Section.objects.create()
628+
with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls:
629+
response = self.client.post("/test_admin/admin/admin_views/section/%d/" % section.pk, {TO_FIELD_VAR: 'name'})
630+
self.assertEqual(response.status_code, 400)
631+
self.assertEqual(len(calls), 1)
632+
602633
def test_allowed_filtering_15103(self):
603634
"""
604635
Regressions test for ticket 15103 - filtering on fields defined in a
@@ -2310,10 +2341,9 @@ def test_with_fk_to_field(self):
23102341
"""Ensure that the to_field GET parameter is preserved when a search
23112342
is performed. Refs #10918.
23122343
"""
2313-
from django.contrib.admin.views.main import TO_FIELD_VAR
2314-
response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR)
2344+
response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR)
23152345
self.assertContains(response, "\n1 user\n")
2316-
self.assertContains(response, '<input type="hidden" name="_to_field" value="username"/>', html=True)
2346+
self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True)
23172347

23182348
def test_exact_matches(self):
23192349
response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')

0 commit comments

Comments
 (0)