Skip to content

Commit

Permalink
[1.1.X] Fix a security issue in the admin. Disclosure and new release…
Browse files Browse the repository at this point in the history
… forthcoming.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.1.X@15035 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
alex committed Dec 23, 2010
1 parent 934dc9e commit 1708483
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 4 deletions.
28 changes: 27 additions & 1 deletion django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import models, transaction
from django.db.models.fields import BLANK_CHOICE_DASH
from django.db.models.related import RelatedObject
from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.datastructures import SortedDict
Expand Down Expand Up @@ -171,6 +173,30 @@ def _declared_fieldsets(self):
return None
declared_fieldsets = property(_declared_fieldsets)

def lookup_allowed(self, lookup):
parts = lookup.split(LOOKUP_SEP)

# Last term in lookup is a query term (__exact, __startswith etc)
# This term can be ignored.
if len(parts) > 1 and parts[-1] in QUERY_TERMS:
parts.pop()

# Special case -- foo__id__exact and foo__id queries are implied
# if foo has been specificially included in the lookup list; so
# drop __id if it is the last part.
if len(parts) > 1 and parts[-1] == self.model._meta.pk.name:
parts.pop()

try:
self.model._meta.get_field_by_name(parts[0])
except FieldDoesNotExist:
# Lookups on non-existants fields are ok, since they're ignored
# later.
return True
else:
clean_lookup = LOOKUP_SEP.join(parts)
return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy

class ModelAdmin(BaseModelAdmin):
"Encapsulates all admin options and functionality for a given model."

Expand Down
8 changes: 7 additions & 1 deletion django/contrib/admin/views/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.admin.filterspecs import FilterSpec
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.util import quote
from django.core.exceptions import SuspiciousOperation
from django.core.paginator import Paginator, InvalidPage
from django.db import models
from django.db.models.query import QuerySet
Expand Down Expand Up @@ -192,13 +193,18 @@ def get_query_set(self):
else:
lookup_params[key] = True

if not self.model_admin.lookup_allowed(key):
raise SuspiciousOperation(
"Filtering by %s not allowed" % key
)

# Apply lookup parameters from the query string.
try:
qs = qs.filter(**lookup_params)
# Naked except! Because we don't have any other way of validating "params".
# They might be invalid if the keyword arguments are incorrect, or if the
# values are not in the correct type, so we might get FieldError, ValueError,
# ValicationError, or ? from a custom field that raises yet something else
# ValicationError, or ? from a custom field that raises yet something else
# when handed impossible data.
except:
raise IncorrectLookupParameters
Expand Down
12 changes: 10 additions & 2 deletions tests/regressiontests/admin_views/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.core.mail import EmailMessage
from django import forms
from django.forms.models import BaseModelFormSet

from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType

Expand Down Expand Up @@ -89,7 +89,7 @@ class ChapterInline(admin.TabularInline):

class ArticleAdmin(admin.ModelAdmin):
list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year')
list_filter = ('date',)
list_filter = ('date', 'section')

def changelist_view(self, request):
"Test that extra_context works"
Expand Down Expand Up @@ -505,6 +505,13 @@ class CyclicTwo(models.Model):
def __unicode__(self):
return self.name

class Album(models.Model):
owner = models.ForeignKey(User)
title = models.CharField(max_length=30)

class AlbumAdmin(admin.ModelAdmin):
list_filter = ['title']

admin.site.register(Article, ArticleAdmin)
admin.site.register(CustomArticle, CustomArticleAdmin)
admin.site.register(Section, save_as=True, inlines=[ArticleInline])
Expand Down Expand Up @@ -547,3 +554,4 @@ def __unicode__(self):
admin.site.register(Book, inlines=[ChapterInline])
admin.site.register(Promo)
admin.site.register(ChapterXtra1)
admin.site.register(Album, AlbumAdmin)
5 changes: 5 additions & 0 deletions tests/regressiontests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import datetime
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.core.files import temp as tempfile
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -289,6 +290,10 @@ def testI18NLanguageNonEnglishFallback(self):
self.assertContains(response, 'Choisir une heure')
deactivate()

def test_disallowed_filtering(self):
self.assertRaises(SuspiciousOperation,
self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy"
)

class SaveAsTests(TestCase):
fixtures = ['admin-views-users.xml','admin-views-person.xml']
Expand Down

0 comments on commit 1708483

Please sign in to comment.