CVE-2014-0483
Description
The administrative interface (contrib.admin) in Django before 1.4.14, 1.5.x before 1.5.9, 1.6.x before 1.6.6, and 1.7 before release candidate 3 does not check if a field represents a relationship between models, which allows remote authenticated users to obtain sensitive information via a to_field parameter in a popup action to an admin change form page, as demonstrated by a /admin/auth/user/?pop=1&t=password URI.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | < 1.4.14 | 1.4.14 |
DjangoPyPI | >= 1.5, < 1.5.9 | 1.5.9 |
DjangoPyPI | >= 1.6, < 1.6.6 | 1.6.6 |
DjangoPyPI | >= 1.7a1, < 1.7c3 | 1.7c3 |
Affected products
42cpe:2.3:a:djangoproject:django:1.6.3:*:*:*:*:*:*:*+ 39 more
- cpe:2.3:a:djangoproject:django:1.6.3:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*range: <=1.4.13
- cpe:2.3:a:djangoproject:django:1.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.6:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.7:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.8:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.9:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.10:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.11:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.12:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:beta1:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:beta2:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:beta3:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:beta4:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:rc1:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5:alpha:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5:beta:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.3:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.6:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.7:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.8:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6:-:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6:beta1:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6:beta2:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6:beta3:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6:beta4:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:rc2:*:*:*:*:*:*
Patches
42a446c896e7c[1.5.x] Prevented data leakage in contrib.admin via query string manipulation.
6 files changed · +76 −5
django/contrib/admin/exceptions.py+6 −0 added@@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class DisallowedModelAdminToField(SuspiciousOperation): + """Invalid to_field was passed to admin view via URL query string""" + pass
django/contrib/admin/options.py+18 −0 modified@@ -275,6 +275,24 @@ def lookup_allowed(self, lookup, value): clean_lookup = LOOKUP_SEP.join(parts) return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy + def to_field_allowed(self, request, to_field): + opts = self.model._meta + + try: + field = opts.get_field(to_field) + except FieldDoesNotExist: + return False + + # Make sure at least one of the models registered for this site + # references this field. + registered_models = self.admin_site._registry + for related_object in opts.get_all_related_objects(): + if (related_object.model in registered_models and + field == related_object.field.rel.get_related_field()): + return True + + return False + def has_add_permission(self, request): """ Returns True if the given request has permission to add an object.
django/contrib/admin/views/main.py+5 −1 modified@@ -12,6 +12,7 @@ from django.utils.http import urlencode from django.contrib.admin import FieldListFilter +from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -58,7 +59,10 @@ def __init__(self, request, model, list_display, list_display_links, self.page_num = 0 self.show_all = ALL_VAR in request.GET self.is_popup = IS_POPUP_VAR in request.GET - self.to_field = request.GET.get(TO_FIELD_VAR) + to_field = request.GET.get(TO_FIELD_VAR) + if to_field and not model_admin.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field) + self.to_field = to_field self.params = dict(request.GET.items()) if PAGE_VAR in self.params: del self.params[PAGE_VAR]
docs/releases/1.4.14.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
docs/releases/1.5.9.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
tests/regressiontests/admin_views/tests.py+17 −4 modified@@ -16,11 +16,12 @@ from django.core.urlresolvers import reverse # Register auth models with the admin. from django.contrib import admin +from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.sites import LOGIN_FORM_KEY from django.contrib.admin.util import quote -from django.contrib.admin.views.main import IS_POPUP_VAR +from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD @@ -557,6 +558,19 @@ def test_disallowed_filtering(self): response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk) self.assertEqual(response.status_code, 200) + def test_disallowed_to_field(self): + with self.assertRaises(DisallowedModelAdminToField): + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'}) + + # Specifying a field that is not refered by any other model registered + # to this admin site should raise an exception. + with self.assertRaises(DisallowedModelAdminToField): + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'}) + + # Specifying a field referenced by another model should be allowed. + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'}) + self.assertEqual(response.status_code, 200) + def test_allowed_filtering_15103(self): """ Regressions test for ticket 15103 - filtering on fields defined in a @@ -2138,10 +2152,9 @@ def test_with_fk_to_field(self): """Ensure that the to_field GET parameter is preserved when a search is performed. Refs #10918. """ - from django.contrib.admin.views.main import TO_FIELD_VAR - response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR) + response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR) self.assertContains(response, "\n1 user\n") - self.assertContains(response, '<input type="hidden" name="t" value="username"/>', html=True) + self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True) def test_exact_matches(self): response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
027bd3486420[1.4.x] Prevented data leakage in contrib.admin via query string manipulation.
5 files changed · +61 −5
django/contrib/admin/exceptions.py+6 −0 added@@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class DisallowedModelAdminToField(SuspiciousOperation): + """Invalid to_field was passed to admin view via URL query string""" + pass
django/contrib/admin/options.py+18 −0 modified@@ -269,6 +269,24 @@ def lookup_allowed(self, lookup, value): clean_lookup = LOOKUP_SEP.join(parts) return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy + def to_field_allowed(self, request, to_field): + opts = self.model._meta + + try: + field = opts.get_field(to_field) + except FieldDoesNotExist: + return False + + # Make sure at least one of the models registered for this site + # references this field. + registered_models = self.admin_site._registry + for related_object in opts.get_all_related_objects(): + if (related_object.model in registered_models and + field == related_object.field.rel.get_related_field()): + return True + + return False + def has_add_permission(self, request): """ Returns True if the given request has permission to add an object.
django/contrib/admin/views/main.py+5 −1 modified@@ -10,6 +10,7 @@ from django.utils.http import urlencode from django.contrib.admin import FieldListFilter +from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -56,7 +57,10 @@ def __init__(self, request, model, list_display, list_display_links, self.page_num = 0 self.show_all = ALL_VAR in request.GET self.is_popup = IS_POPUP_VAR in request.GET - self.to_field = request.GET.get(TO_FIELD_VAR) + to_field = request.GET.get(TO_FIELD_VAR) + if to_field and not model_admin.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field) + self.to_field = to_field self.params = dict(request.GET.items()) if PAGE_VAR in self.params: del self.params[PAGE_VAR]
docs/releases/1.4.14.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
tests/regressiontests/admin_views/tests.py+17 −4 modified@@ -13,11 +13,12 @@ from django.core.urlresolvers import reverse # Register auth models with the admin. from django.contrib import admin +from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.sites import LOGIN_FORM_KEY from django.contrib.admin.util import quote -from django.contrib.admin.views.main import IS_POPUP_VAR +from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD @@ -572,6 +573,19 @@ def test_disallowed_filtering(self): response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk) self.assertEqual(response.status_code, 200) + def test_disallowed_to_field(self): + with self.assertRaises(DisallowedModelAdminToField): + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'}) + + # Specifying a field that is not refered by any other model registered + # to this admin site should raise an exception. + with self.assertRaises(DisallowedModelAdminToField): + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'}) + + # Specifying a field referenced by another model should be allowed. + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'}) + self.assertEqual(response.status_code, 200) + def test_allowed_filtering_15103(self): """ Regressions test for ticket 15103 - filtering on fields defined in a @@ -2061,10 +2075,9 @@ def test_with_fk_to_field(self): """Ensure that the to_field GET parameter is preserved when a search is performed. Refs #10918. """ - from django.contrib.admin.views.main import TO_FIELD_VAR - response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR) + response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR) self.assertContains(response, "\n1 user\n") - self.assertContains(response, '<input type="hidden" name="t" value="username"/>', html=True) + self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True) def test_exact_matches(self): response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
f7c494f25062[1.6.x] Prevented data leakage in contrib.admin via query string manipulation.
8 files changed · +94 −5
django/contrib/admin/exceptions.py+5 −0 modified@@ -4,3 +4,8 @@ class DisallowedModelAdminLookup(SuspiciousOperation): """Invalid filter was passed to admin view via URL querystring""" pass + + +class DisallowedModelAdminToField(SuspiciousOperation): + """Invalid to_field was passed to admin view via URL query string""" + pass
django/contrib/admin/options.py+18 −0 modified@@ -327,6 +327,24 @@ def lookup_allowed(self, lookup, value): clean_lookup = LOOKUP_SEP.join(parts) return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy + def to_field_allowed(self, request, to_field): + opts = self.model._meta + + try: + field = opts.get_field(to_field) + except FieldDoesNotExist: + return False + + # Make sure at least one of the models registered for this site + # references this field. + registered_models = self.admin_site._registry + for related_object in opts.get_all_related_objects(): + if (related_object.model in registered_models and + field in related_object.field.foreign_related_fields): + return True + + return False + def has_add_permission(self, request): """ Returns True if the given request has permission to add an object.
django/contrib/admin/views/main.py+5 −2 modified@@ -14,7 +14,7 @@ from django.utils.http import urlencode from django.contrib.admin import FieldListFilter -from django.contrib.admin.exceptions import DisallowedModelAdminLookup +from django.contrib.admin.exceptions import DisallowedModelAdminLookup, DisallowedModelAdminToField from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -90,7 +90,10 @@ def __init__(self, request, model, list_display, list_display_links, self.page_num = 0 self.show_all = ALL_VAR in request.GET self.is_popup = _is_changelist_popup(request) - self.to_field = request.GET.get(TO_FIELD_VAR) + to_field = request.GET.get(TO_FIELD_VAR) + if to_field and not model_admin.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field) + self.to_field = to_field self.params = dict(request.GET.items()) if PAGE_VAR in self.params: del self.params[PAGE_VAR]
docs/ref/exceptions.txt+1 −0 modified@@ -56,6 +56,7 @@ SuspiciousOperation * DisallowedHost * DisallowedModelAdminLookup + * DisallowedModelAdminToField * DisallowedRedirect * InvalidSessionKey * SuspiciousFileOperation
docs/releases/1.4.14.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
docs/releases/1.5.9.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
docs/releases/1.6.6.txt+15 −0 modified@@ -48,6 +48,21 @@ requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?_popup=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified. + Bugfixes ========
tests/admin_views/tests.py+20 −3 modified@@ -14,6 +14,7 @@ from django.contrib import admin from django.contrib.auth import get_permission_codename from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.contrib.admin.views.main import TO_FIELD_VAR from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.sites import LOGIN_FORM_KEY from django.contrib.admin.templatetags.admin_urls import add_preserved_filters @@ -577,6 +578,23 @@ def test_disallowed_filtering(self): response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk) self.assertEqual(response.status_code, 200) + def test_disallowed_to_field(self): + with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'}) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) + + # Specifying a field that is not refered by any other model registered + # to this admin site should raise an exception. + with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'}) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) + + # Specifying a field referenced by another model should be allowed. + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'}) + self.assertEqual(response.status_code, 200) + def test_allowed_filtering_15103(self): """ Regressions test for ticket 15103 - filtering on fields defined in a @@ -2204,10 +2222,9 @@ def test_with_fk_to_field(self): """Ensure that the to_field GET parameter is preserved when a search is performed. Refs #10918. """ - from django.contrib.admin.views.main import TO_FIELD_VAR - response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR) + response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR) self.assertContains(response, "\n1 user\n") - self.assertContains(response, '<input type="hidden" name="t" value="username"/>', html=True) + self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True) def test_exact_matches(self): response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
2b31342cdf14[1.7.x] Prevented data leakage in contrib.admin via query string manipulation.
8 files changed · +115 −7
django/contrib/admin/exceptions.py+5 −0 modified@@ -4,3 +4,8 @@ class DisallowedModelAdminLookup(SuspiciousOperation): """Invalid filter was passed to admin view via URL querystring""" pass + + +class DisallowedModelAdminToField(SuspiciousOperation): + """Invalid to_field was passed to admin view via URL query string""" + pass
django/contrib/admin/options.py+24 −2 modified@@ -11,6 +11,7 @@ from django.contrib.admin import validation from django.contrib.admin.checks import (BaseModelAdminChecks, ModelAdminChecks, InlineModelAdminChecks) +from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.utils import (quote, unquote, flatten_fieldsets, get_deleted_objects, model_format_dict, NestedObjects, lookup_needs_distinct) @@ -434,6 +435,24 @@ def lookup_allowed(self, lookup, value): valid_lookups.append(filter_item) return clean_lookup in valid_lookups + def to_field_allowed(self, request, to_field): + opts = self.model._meta + + try: + field = opts.get_field(to_field) + except FieldDoesNotExist: + return False + + # Make sure at least one of the models registered for this site + # references this field. + registered_models = self.admin_site._registry + for related_object in opts.get_all_related_objects(): + if (related_object.model in registered_models and + field in related_object.field.foreign_related_fields): + return True + + return False + def has_add_permission(self, request): """ Returns True if the given request has permission to add an object. @@ -1325,6 +1344,10 @@ def get_changeform_initial_data(self, request): @transaction.atomic def changeform_view(self, request, object_id=None, form_url='', extra_context=None): + to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) + if to_field and not self.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field) + model = self.model opts = model._meta add = object_id is None @@ -1397,8 +1420,7 @@ def changeform_view(self, request, object_id=None, form_url='', extra_context=No original=obj, is_popup=(IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET), - to_field=request.POST.get(TO_FIELD_VAR, - request.GET.get(TO_FIELD_VAR)), + to_field=to_field, media=media, inline_admin_formsets=inline_formsets, errors=helpers.AdminErrorList(form, formsets),
django/contrib/admin/views/main.py+7 −2 modified@@ -14,7 +14,9 @@ from django.utils.http import urlencode from django.contrib.admin import FieldListFilter -from django.contrib.admin.exceptions import DisallowedModelAdminLookup +from django.contrib.admin.exceptions import ( + DisallowedModelAdminLookup, DisallowedModelAdminToField, +) from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, TO_FIELD_VAR from django.contrib.admin.utils import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -89,7 +91,10 @@ def __init__(self, request, model, list_display, list_display_links, self.page_num = 0 self.show_all = ALL_VAR in request.GET self.is_popup = _is_changelist_popup(request) - self.to_field = request.GET.get(TO_FIELD_VAR) + to_field = request.GET.get(TO_FIELD_VAR) + if to_field and not model_admin.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field) + self.to_field = to_field self.params = dict(request.GET.items()) if PAGE_VAR in self.params: del self.params[PAGE_VAR]
docs/ref/exceptions.txt+1 −0 modified@@ -56,6 +56,7 @@ SuspiciousOperation * DisallowedHost * DisallowedModelAdminLookup + * DisallowedModelAdminToField * DisallowedRedirect * InvalidSessionKey * SuspiciousFileOperation
docs/releases/1.4.14.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
docs/releases/1.5.9.txt+15 −0 modified@@ -47,3 +47,18 @@ and the ``RemoteUserBackend``, a change to the ``REMOTE_USER`` header between requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. + +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?pop=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified.
docs/releases/1.6.6.txt+15 −0 modified@@ -48,6 +48,21 @@ requests without an intervening logout could result in the prior user's session being co-opted by the subsequent user. The middleware now logs the user out on a failed login attempt. +Data leakage via query string manipulation in ``contrib.admin`` +=============================================================== + +In older versions of Django it was possible to reveal any field's data by +modifying the "popup" and "to_field" parameters of the query string on an admin +change form page. For example, requesting a URL like +``/admin/auth/user/?_popup=1&t=password`` and viewing the page's HTML allowed +viewing the password hash of each user. While the admin requires users to have +permissions to view the change form pages in the first place, this could leak +data if you rely on users having access to view only certain fields on a model. + +To address the issue, an exception will now be raised if a ``to_field`` value +that isn't a related field to a model that has been registered with the admin +is specified. + Bugfixes ========
tests/admin_views/tests.py+33 −3 modified@@ -18,6 +18,7 @@ from django.contrib.admin import ModelAdmin from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.models import LogEntry, DELETION +from django.contrib.admin.options import TO_FIELD_VAR from django.contrib.admin.templatetags.admin_static import static from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase @@ -599,6 +600,36 @@ def test_disallowed_filtering(self): response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk) self.assertEqual(response.status_code, 200) + def test_disallowed_to_field(self): + with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'}) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) + + # Specifying a field that is not refered by any other model registered + # to this admin site should raise an exception. + with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'}) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) + + # Specifying a field referenced by another model should be allowed. + response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'}) + self.assertEqual(response.status_code, 200) + + # We also want to prevent the add and change view from leaking a + # disallowed field value. + with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: + response = self.client.post("/test_admin/admin/admin_views/section/add/", {TO_FIELD_VAR: 'name'}) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) + + section = Section.objects.create() + with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: + response = self.client.post("/test_admin/admin/admin_views/section/%d/" % section.pk, {TO_FIELD_VAR: 'name'}) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) + def test_allowed_filtering_15103(self): """ Regressions test for ticket 15103 - filtering on fields defined in a @@ -2310,10 +2341,9 @@ def test_with_fk_to_field(self): """Ensure that the to_field GET parameter is preserved when a search is performed. Refs #10918. """ - from django.contrib.admin.views.main import TO_FIELD_VAR - response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR) + response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR) self.assertContains(response, "\n1 user\n") - self.assertContains(response, '<input type="hidden" name="_to_field" value="username"/>', html=True) + self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True) def test_exact_matches(self): response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
17- github.com/django/django/commit/2b31342cdf14fc20e07c43d258f1e7334ad664a6nvdExploitPatchWEB
- github.com/advisories/GHSA-rw75-m7gp-92m3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-0483ghsaADVISORY
- www.djangoproject.com/weblog/2014/aug/20/security/nvdVendor Advisory
- lists.opensuse.org/opensuse-updates/2014-09/msg00023.htmlnvdWEB
- www.debian.org/security/2014/dsa-3010nvdWEB
- github.com/django/django/commit/027bd348642007617518379f8b02546abacaa6e0ghsaWEB
- github.com/django/django/commit/2a446c896e7c814661fb9c4f212b071b2a7fa446ghsaWEB
- github.com/django/django/commit/f7c494f2506250b8cb5923714360a3642ed63e0fghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2014-7.yamlghsaWEB
- web.archive.org/web/20151016194735/http://secunia.com/advisories/61276ghsaWEB
- web.archive.org/web/20151016202523/http://secunia.com/advisories/59782ghsaWEB
- web.archive.org/web/20151023143840/http://secunia.com/advisories/61281ghsaWEB
- www.djangoproject.com/weblog/2014/aug/20/securityghsaWEB
- secunia.com/advisories/59782nvd
- secunia.com/advisories/61276nvd
- secunia.com/advisories/61281nvd
News mentions
0No linked articles in our index yet.