Skip to content

Instantly share code, notes, and snippets.

@purple4reina
Last active August 29, 2015 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save purple4reina/53e92db456a3a1c5f488 to your computer and use it in GitHub Desktop.
Save purple4reina/53e92db456a3a1c5f488 to your computer and use it in GitHub Desktop.
Django Safe Template
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