Last active
August 29, 2015 14:22
-
-
Save purple4reina/53e92db456a3a1c5f488 to your computer and use it in GitHub Desktop.
Django Safe Template
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.template import Template | |
class SafeTemplate(Template): | |
""" | |
Subclass of the regular django Template but disallows rendering anything | |
that will call a method on a class thus making it safe for use as a user | |
editable object | |
Examples: | |
# this will return the template as expected | |
>> template = SafeTemplate('{{ server.hostname }}', server=server) | |
>> template.render(context) | |
'reina-1' | |
# but since this calls a method on server, it will return an empty | |
# string | |
>> template = SafeTemplate('{{ server.power_off }}', server=server) | |
>> template.render(context) | |
'' | |
Discussion: | |
Django offers a way of specifying if a method should not be called | |
during template rendering. See alters_data in the django docs which is | |
an attr that can be set on any class method. However, django defaults | |
this attr to False for all methods. If we were to use this route of | |
solving the problem at hand, we would have to set this attr all over | |
the place. That's not reasonable. | |
Allowed: model fields, custom fields, and properties on a server | |
Disallowed: all methods on django's RelatedManager objects, all things | |
that return true for inspect.ismethod = True | |
Eventually it would be nice to include a white/blacklist to speed this | |
process up | |
""" | |
def render(self, context, autoescape=True): | |
""" | |
Rebuild the template.nodelist to filter out any Nodes we don't like | |
This accepts any TextNode types, conditionally accepts any VariableNode | |
types, and rejects all other node types (ex: comments, forloops, etc) | |
""" | |
final_nodelist = DebugNodeList() | |
for node in self.nodelist: | |
if isinstance(node, VariableNode): | |
# this is some sort of variable call so we need to confirm that | |
# its allowable | |
lookups = node.filter_expression.var.lookups | |
if not self.alters_data(lookups, context): | |
final_nodelist.append(node) | |
elif isinstance(node, TextNode): | |
# this is just text so we'll allow it | |
final_nodelist.append(node) | |
self.nodelist = final_nodelist | |
rendered_data = super(SafeTemplate, self).render(context) | |
if not autoescape: | |
# When rendering a template, django autoescapes by default. This | |
# means that all quotation marks, greaterthan/lessthan signs, etc | |
# will be escaped. In some cases we may not want this (ex. if this | |
# is a script to be run on a server). | |
rendered_data = HTMLParser().unescape(rendered_data) | |
return rendered_data | |
def alters_data(self, var_lookups, context): | |
""" | |
Determine if the list of var_lookups would produce a method call, | |
return True if so | |
`var_lookups` is a tuple of strings as returned by | |
node.filter_expression.var.lookups (see render() above). This list is | |
basically the template node's string value split at '.' | |
Examples: | |
When var_lookups = ('server', 'hostname'), the django template | |
rendering would call server.hostname which we will allow | |
When var_lookups = ('server', 'environment', 'server_set', 'all', | |
'delete') the user is trying to delete all servers from this | |
environment. Since calling `all` is a method, it is disallowed | |
""" | |
# The first lookup will be an object found in the context dictionary | |
this_lookup = context[var_lookups[0]] | |
for lookup in var_lookups[1:]: | |
try: | |
attr = this_lookup.__getattribute__(lookup) | |
except AttributeError: | |
# If this is a custom field on a server, then __getattribute__ | |
# throws an AttributeError. Therefore, we must call | |
# get_value_for_custom_field directly. | |
attr = this_lookup.get_value_for_custom_field(lookup) | |
if inspect.ismethod(attr): | |
return True | |
this_lookup = attr | |
return False |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment