Fixing Django 1.2.4’s SuspiciousOperation on filtering
As of Django 1.2.4, you can no longer create filtered links to other models in the admin. For example, here’s something I use to show all siblings of a page:
[toggle code]
-
class PageAdmin(models.ModelAdmin):
- list_display = (…, 'siblings')
- …
-
def siblings(self, page):
-
if page.parent:
- link = '/admin/pages/page/?parent__id__exact=' + unicode(page.parent.id)
- text = '<a href="' + link + '">siblings</a>'
- return text
- return None
-
if page.parent:
- siblings.short_description='Siblings'
- siblings.allow_tags = True
Because this can result in information leakage, it’s been disabled as of 1.2.4. You’ll end up with an error message that looks like:
SuspiciousOperation at /admin/pages/page/
Filtering by parent__id__exact not allowed
As far as I can tell, the only filters that are allowed are the ones that appear in the sidebar. But for something like this, where there are thousands of possibilities, putting it in the sidebar would be disastrous.
The announcement was unhelpful, but from the source, the ModelAdmin class checks for valid filters using a lookup_allowed method. That method returns True if the lookup should be allowed, and False if it should not. We can override that method to add a new keyword to ModelAdmin, valid_lookups, that accepts a list of lookups that should be allowed.
[toggle code]
-
class SmarterModelAdmin(admin.ModelAdmin):
- valid_lookups = ()
-
def lookup_allowed(self, lookup, *args, **kwargs):
-
if lookup.startswith(self.valid_lookups):
- return True
- return super(SmarterModelAdmin, self).lookup_allowed(lookup, *args, **kwargs)
-
if lookup.startswith(self.valid_lookups):
This sets up an empty tuple called valid_lookups; it can be assigned to just like any other option in ModelAdmin instances. It uses startswith so that if you want to enable a field for filtering you don’t have to enable every permutation of it. Just the field name will do; or you can enable the specific filter (such as parent__id__exact).
Because the method uses startswith, valid_lookups needs to be a tuple.
[toggle code]
-
class PageAdmin(SmarterModelAdmin):
- valid_lookups = ('parent',)
- …
This will also enable fields with similar names. For example, if you enable all filters for “key” and you also have a “keyword” lookup, enabling “key” will enable both of them. Add an underscore or two if you need to avoid that. For example, “key__”.
Judging from my reading of the Django 1.3 release notes, there will be a mechanism for doing something similar to this in 1.3, but I’m not sure.
Update: fixed bad tuple from ('parent') to ('parent',) noticed by Jonathan Hartley. It still worked before because a bad tuple turns into a string, and startswith works with both strings and tuples.
- February 8, 2011: lookup_allowed gets new parameter for value
-
I’ve updated the lookup_allowed method in SmarterModelAdmin because it looks like (a) there won’t be an official solution in Django 1.3, and lookup_allowed is going to get a new parameter.
[toggle code]
-
class SmarterModelAdmin(admin.ModelAdmin):
- valid_lookups = ()
-
def lookup_allowed(self, lookup, *args, **kwargs):
-
if lookup.startswith(self.valid_lookups):
- return True
- return super(SmarterModelAdmin, self).lookup_allowed(lookup, *args, **kwargs)
-
if lookup.startswith(self.valid_lookups):
Overall this seems like a good change to me. If done right it allows filtering based on value as well as on field.
Hopefully, a later Django will alleviate the need to use an undocumented override. The main reason I didn’t put much work in this solution is that I thought I saw something about there already being a solution in 1.3; sounds like that isn’t the case.
This was a little annoying:
It’s unfortunate that people are externally documenting the “fix” for the security problem to be “remove the security”, but there’s not much we can do beyond documenting the change.
That is of course untrue. You could provide a sanctioned method for allowing filters on lookups that don’t appear in a list_filter. Saying that adding a valid_lookups property is “removing the security” is saying that their fix isn’t a fix at all, since it removes the security from list_filter fields. In both cases the code is just looking at a list of fields and relations for which admin filtering should be allowed.
Being able to drill into a database is useful, as the existence of list_filter shows. The “hack” that allows building custom admin queries to drill into field values in the admin display is well-known and well-promulgated and if it isn’t in the documentation, it is from the time when the documentation was “read the source”.
-
class SmarterModelAdmin(admin.ModelAdmin):
- Django 1.2.4 Security releases issued: James Bennett at Django
- “To remedy this, django.contrib.admin will now validate that querystring lookup arguments either specify only fields on the model being viewed, or cross relations which have been explicitly whitelisted by the application developer using the pre-existing mechanism not mentioned above.”
More Django
- Converting an existing Django model to Django-MPTT
- Using a SQL database to mimic a filesystem will, eventually, create bottlenecks when it comes to traversing the filesystem. One solution is modified preordered tree traversal, which saves the tree structure in an easily-used manner inside the model.
- Two search bookmarklets for Django
- Bookmarklets—JavaScript code in a bookmark—can make working with big Django databases much easier.
- Fixing Django’s feed generator without hacking Django
- It looks like it’s going to be a while before the RSS feed generator in Django is going to get fixed, so I looked into subclassing as a way of getting a working guid in my Django RSS feeds.
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- 29 more pages with the topic Django, and other related pages
This security "fix" is a complete pain in the a. Thanks for providing something that steps around it so easily (and still securely).
Oli Warner at 2:34 p.m. May 17th, 2011
V0iJN
I wouldn’t say it’s secure, just that you have to make the decision to enable it, which means that it won’t be a surprise that URLs can be used for introspection.
I’ve seen other solutions that just override lookup_allowed and return True, which is probably not a good idea.
capvideo at 4:38 a.m. May 18th, 2011
tVAhq
Thanks for the solution, this saved me scratching my head for hours. I'm still on Django 1.2, so I think I need all this.
A superficial refinement: If valid_lookups is to define a *collection* of valid start-of-fieldnames, then we need to change the misleadingly-parenthesized string ('parent') into the tuple ('parent',) and then change the 'if' in lookup_allowed to:
if any(lookup.startswith(valid) for valid in self.valid_lookups):
return True
Jonathan Hartley in London, UK at 4:20 a.m. October 5th, 2012
mHF8v