티스토리 수익 글 보기

티스토리 수익 글 보기

[1.5.x] Prevented data leakage in contrib.admin via query string mani… · django/django@2a446c8 · GitHub
Skip to content
/ django Public

Commit 2a446c8

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

File tree

6 files changed

+76
5
lines changed

6 files changed

+76
5
lines changed

django/contrib/admin/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.core.exceptions import SuspiciousOperation
2+
3+
4+
class DisallowedModelAdminToField(SuspiciousOperation):
5+
"""Invalid to_field was passed to admin view via URL query string"""
6+
pass

django/contrib/admin/options.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,24 @@ def lookup_allowed(self, lookup, value):
275275
clean_lookup = LOOKUP_SEP.join(parts)
276276
return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
277277

278+
def to_field_allowed(self, request, to_field):
279+
opts = self.model._meta
280+
281+
try:
282+
field = opts.get_field(to_field)
283+
except FieldDoesNotExist:
284+
return False
285+
286+
# Make sure at least one of the models registered for this site
287+
# references this field.
288+
registered_models = self.admin_site._registry
289+
for related_object in opts.get_all_related_objects():
290+
if (related_object.model in registered_models and
291+
field == related_object.field.rel.get_related_field()):
292+
return True
293+
294+
return False
295+
278296
def has_add_permission(self, request):
279297
"""
280298
Returns True if the given request has permission to add an object.

django/contrib/admin/views/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django.utils.http import urlencode
1313

1414
from django.contrib.admin import FieldListFilter
15+
from django.contrib.admin.exceptions import DisallowedModelAdminToField
1516
from django.contrib.admin.options import IncorrectLookupParameters
1617
from django.contrib.admin.util import (quote, get_fields_from_path,
1718
lookup_needs_distinct, prepare_lookup_value)
@@ -58,7 +59,10 @@ def __init__(self, request, model, list_display, list_display_links,
5859
self.page_num = 0
5960
self.show_all = ALL_VAR in request.GET
6061
self.is_popup = IS_POPUP_VAR in request.GET
61-
self.to_field = request.GET.get(TO_FIELD_VAR)
62+
to_field = request.GET.get(TO_FIELD_VAR)
63+
if to_field and not model_admin.to_field_allowed(request, to_field):
64+
raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
65+
self.to_field = to_field
6266
self.params = dict(request.GET.items())
6367
if PAGE_VAR in self.params:
6468
del self.params[PAGE_VAR]

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.

tests/regressiontests/admin_views/tests.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
from django.core.urlresolvers import reverse
1717
# Register auth models with the admin.
1818
from django.contrib import admin
19+
from django.contrib.admin.exceptions import DisallowedModelAdminToField
1920
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
2021
from django.contrib.admin.models import LogEntry, DELETION
2122
from django.contrib.admin.sites import LOGIN_FORM_KEY
2223
from django.contrib.admin.util import quote
23-
from django.contrib.admin.views.main import IS_POPUP_VAR
24+
from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
2425
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
2526
from django.contrib.auth import REDIRECT_FIELD_NAME
2627
from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD
@@ -557,6 +558,19 @@ def test_disallowed_filtering(self):
557558
response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
558559
self.assertEqual(response.status_code, 200)
559560

561+
def test_disallowed_to_field(self):
562+
with self.assertRaises(DisallowedModelAdminToField):
563+
response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'})
564+
565+
# Specifying a field that is not refered by any other model registered
566+
# to this admin site should raise an exception.
567+
with self.assertRaises(DisallowedModelAdminToField):
568+
response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'})
569+
570+
# Specifying a field referenced by another model should be allowed.
571+
response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'})
572+
self.assertEqual(response.status_code, 200)
573+
560574
def test_allowed_filtering_15103(self):
561575
"""
562576
Regressions test for ticket 15103 - filtering on fields defined in a
@@ -2138,10 +2152,9 @@ def test_with_fk_to_field(self):
21382152
"""Ensure that the to_field GET parameter is preserved when a search
21392153
is performed. Refs #10918.
21402154
"""
2141-
from django.contrib.admin.views.main import TO_FIELD_VAR
2142-
response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR)
2155+
response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR)
21432156
self.assertContains(response, "\n1 user\n")
2144-
self.assertContains(response, '<input type="hidden" name="t" value="username"/>', html=True)
2157+
self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True)
21452158

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

0 commit comments

Comments
 (0)