Fixed #17906 - Autoescaping {% cycle %} and {% firstof %} templatetags.

This commit adds "future" version of these two tags with auto-escaping
enabled.
This commit is contained in:
Vladimir A Filonov 2013-02-23 15:07:21 +01:00 committed by Aymeric Augustin
parent a61dbd6219
commit f49e9a517f
6 changed files with 173 additions and 31 deletions

View File

@ -5,13 +5,15 @@ import sys
import re import re
from datetime import datetime from datetime import datetime
from itertools import groupby, cycle as itertools_cycle from itertools import groupby, cycle as itertools_cycle
import warnings
from django.conf import settings from django.conf import settings
from django.template.base import (Node, NodeList, Template, Context, Library, from django.template.base import (Node, NodeList, Template, Context, Library,
TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary, TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary,
BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END,
SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END,
VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re) VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re,
_render_value_in_context)
from django.template.smartif import IfParser, Literal from django.template.smartif import IfParser, Literal
from django.template.defaultfilters import date from django.template.defaultfilters import date
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
@ -54,15 +56,15 @@ class CsrfTokenNode(Node):
# misconfiguration, so we raise a warning # misconfiguration, so we raise a warning
from django.conf import settings from django.conf import settings
if settings.DEBUG: if settings.DEBUG:
import warnings
warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.")
return '' return ''
class CycleNode(Node): class CycleNode(Node):
def __init__(self, cyclevars, variable_name=None, silent=False): def __init__(self, cyclevars, variable_name=None, silent=False, escape=False):
self.cyclevars = cyclevars self.cyclevars = cyclevars
self.variable_name = variable_name self.variable_name = variable_name
self.silent = silent self.silent = silent
self.escape = escape # only while the "future" version exists
def render(self, context): def render(self, context):
if self not in context.render_context: if self not in context.render_context:
@ -74,7 +76,9 @@ class CycleNode(Node):
context[self.variable_name] = value context[self.variable_name] = value
if self.silent: if self.silent:
return '' return ''
return value if not self.escape:
value = mark_safe(value)
return _render_value_in_context(value, context)
class DebugNode(Node): class DebugNode(Node):
def render(self, context): def render(self, context):
@ -97,14 +101,17 @@ class FilterNode(Node):
return filtered return filtered
class FirstOfNode(Node): class FirstOfNode(Node):
def __init__(self, vars): def __init__(self, variables, escape=False):
self.vars = vars self.vars = variables
self.escape = escape # only while the "future" version exists
def render(self, context): def render(self, context):
for var in self.vars: for var in self.vars:
value = var.resolve(context, True) value = var.resolve(context, True)
if value: if value:
return smart_text(value) if not self.escape:
value = mark_safe(value)
return _render_value_in_context(value, context)
return '' return ''
class ForNode(Node): class ForNode(Node):
@ -508,7 +515,7 @@ def comment(parser, token):
return CommentNode() return CommentNode()
@register.tag @register.tag
def cycle(parser, token): def cycle(parser, token, escape=False):
""" """
Cycles among the given strings each time this tag is encountered. Cycles among the given strings each time this tag is encountered.
@ -541,6 +548,11 @@ def cycle(parser, token):
{% endfor %} {% endfor %}
""" """
if not escape:
warnings.warn(
"'The syntax for the `cycle` template tag is changing. Load it "
"from the `future` tag library to start using the new behavior.",
PendingDeprecationWarning, stacklevel=2)
# Note: This returns the exact same node on each {% cycle name %} call; # Note: This returns the exact same node on each {% cycle name %} call;
# that is, the node object returned from {% cycle a b c as name %} and the # that is, the node object returned from {% cycle a b c as name %} and the
@ -588,13 +600,13 @@ def cycle(parser, token):
if as_form: if as_form:
name = args[-1] name = args[-1]
values = [parser.compile_filter(arg) for arg in args[1:-2]] values = [parser.compile_filter(arg) for arg in args[1:-2]]
node = CycleNode(values, name, silent=silent) node = CycleNode(values, name, silent=silent, escape=escape)
if not hasattr(parser, '_namedCycleNodes'): if not hasattr(parser, '_namedCycleNodes'):
parser._namedCycleNodes = {} parser._namedCycleNodes = {}
parser._namedCycleNodes[name] = node parser._namedCycleNodes[name] = node
else: else:
values = [parser.compile_filter(arg) for arg in args[1:]] values = [parser.compile_filter(arg) for arg in args[1:]]
node = CycleNode(values) node = CycleNode(values, escape=escape)
return node return node
@register.tag @register.tag
@ -643,7 +655,7 @@ def do_filter(parser, token):
return FilterNode(filter_expr, nodelist) return FilterNode(filter_expr, nodelist)
@register.tag @register.tag
def firstof(parser, token): def firstof(parser, token, escape=False):
""" """
Outputs the first variable passed that is not False, without escaping. Outputs the first variable passed that is not False, without escaping.
@ -657,11 +669,11 @@ def firstof(parser, token):
{% if var1 %} {% if var1 %}
{{ var1|safe }} {{ var1|safe }}
{% else %}{% if var2 %} {% elif var2 %}
{{ var2|safe }} {{ var2|safe }}
{% else %}{% if var3 %} {% elif var3 %}
{{ var3|safe }} {{ var3|safe }}
{% endif %}{% endif %}{% endif %} {% endif %}
but obviously much cleaner! but obviously much cleaner!
@ -677,10 +689,16 @@ def firstof(parser, token):
{% endfilter %} {% endfilter %}
""" """
if not escape:
warnings.warn(
"'The syntax for the `firstof` template tag is changing. Load it "
"from the `future` tag library to start using the new behavior.",
PendingDeprecationWarning, stacklevel=2)
bits = token.split_contents()[1:] bits = token.split_contents()[1:]
if len(bits) < 1: if len(bits) < 1:
raise TemplateSyntaxError("'firstof' statement requires at least one argument") raise TemplateSyntaxError("'firstof' statement requires at least one argument")
return FirstOfNode([parser.compile_filter(bit) for bit in bits]) return FirstOfNode([parser.compile_filter(bit) for bit in bits], escape=escape)
@register.tag('for') @register.tag('for')
def do_for(parser, token): def do_for(parser, token):

View File

@ -1,14 +1,65 @@
from django.template import Library from django.template import Library
from django.template.defaulttags import url as default_url, ssi as default_ssi from django.template import defaulttags
register = Library() register = Library()
@register.tag @register.tag
def ssi(parser, token): def ssi(parser, token):
# Used for deprecation path during 1.3/1.4, will be removed in 2.0 # Used for deprecation path during 1.3/1.4, will be removed in 2.0
return default_ssi(parser, token) return defaulttags.ssi(parser, token)
@register.tag @register.tag
def url(parser, token): def url(parser, token):
# Used for deprecation path during 1.3/1.4, will be removed in 2.0 # Used for deprecation path during 1.3/1.4, will be removed in 2.0
return default_url(parser, token) return defaulttags.url(parser, token)
@register.tag
def cycle(parser, token):
"""
This is the future version of `cycle` with auto-escaping.
By default all strings are escaped.
If you want to disable auto-escaping of variables you can use:
{% autoescape off %}
{% cycle var1 var2 var3 as somecycle %}
{% autoescape %}
Or if only some variables should be escaped, you can use:
{% cycle var1 var2|safe var3|safe as somecycle %}
"""
return defaulttags.cycle(parser, token, escape=True)
@register.tag
def firstof(parser, token):
"""
This is the future version of `firstof` with auto-escaping.
This is equivalent to:
{% if var1 %}
{{ var1 }}
{% elif var2 %}
{{ var2 }}
{% elif var3 %}
{{ var3 }}
{% endif %}
If you want to disable auto-escaping of variables you can use:
{% autoescape off %}
{% firstof var1 var2 var3 "<strong>fallback value</strong>" %}
{% autoescape %}
Or if only some variables should be escaped, you can use:
{% firstof var1 var2|safe var3 "<strong>fallback value</strong>"|safe %}
"""
return defaulttags.firstof(parser, token, escape=True)

View File

@ -323,6 +323,10 @@ these changes.
1.8 1.8
--- ---
* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their
arguments. In 1.6 and 1.7, this behavior is provided by the version of these
tags in the ``future`` template tag library.
* The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the * The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the
:class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to
your :setting:`MIDDLEWARE_CLASSES` setting instead. your :setting:`MIDDLEWARE_CLASSES` setting instead.

View File

@ -147,9 +147,8 @@ You can use any number of values in a ``{% cycle %}`` tag, separated by spaces.
Values enclosed in single (``'``) or double quotes (``"``) are treated as Values enclosed in single (``'``) or double quotes (``"``) are treated as
string literals, while values without quotes are treated as template variables. string literals, while values without quotes are treated as template variables.
Note that the variables included in the cycle will not be escaped. Note that currently the variables included in the cycle will not be escaped.
This is because template tags do not escape their content. Any HTML or Any HTML or Javascript code contained in the printed variable will be rendered
Javascript code contained in the printed variable will be rendered
as-is, which could potentially lead to security issues. as-is, which could potentially lead to security issues.
For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior
@ -190,6 +189,22 @@ call to ``{% cycle %}`` doesn't specify silent::
{% cycle 'row1' 'row2' as rowcolors silent %} {% cycle 'row1' 'row2' as rowcolors silent %}
{% cycle rowcolors %} {% cycle rowcolors %}
.. versionchanged:: 1.6
To improve safety, future versions of ``cycle`` will automatically escape
their output. You're encouraged to activate this behavior by loading
``cycle`` from the ``future`` template library::
{% load cycle from future %}
When using the ``future`` version, you can disable auto-escaping with::
{% for o in some_list %}
<tr class="{% autoescape off %}{% cycle rowvalue1 rowvalue2 %}{% endautoescape %}">
...
</tr>
{% endfor %}
.. templatetag:: debug .. templatetag:: debug
debug debug
@ -257,28 +272,44 @@ This is equivalent to::
{% if var1 %} {% if var1 %}
{{ var1|safe }} {{ var1|safe }}
{% else %}{% if var2 %} {% elif var2 %}
{{ var2|safe }} {{ var2|safe }}
{% else %}{% if var3 %} {% elif var3 %}
{{ var3|safe }} {{ var3|safe }}
{% endif %}{% endif %}{% endif %} {% endif %}
You can also use a literal string as a fallback value in case all You can also use a literal string as a fallback value in case all
passed variables are False:: passed variables are False::
{% firstof var1 var2 var3 "fallback value" %} {% firstof var1 var2 var3 "fallback value" %}
Note that the variables included in the firstof tag will not be Note that currently the variables included in the firstof tag will not be
escaped. This is because template tags do not escape their content. escaped. Any HTML or Javascript code contained in the printed variable will be
Any HTML or Javascript code contained in the printed variable will be rendered as-is, which could potentially lead to security issues. If you need
rendered as-is, which could potentially lead to security issues. If you to escape the variables in the firstof tag, you must do so explicitly::
need to escape the variables in the firstof tag, you must do so
explicitly::
{% filter force_escape %} {% filter force_escape %}
{% firstof var1 var2 var3 "fallback value" %} {% firstof var1 var2 var3 "fallback value" %}
{% endfilter %} {% endfilter %}
.. versionchanged:: 1.6
To improve safety, future versions of ``firstof`` will automatically escape
their output. You're encouraged to activate this behavior by loading
``firstof`` from the ``future`` template library::
{% load firstof from future %}
When using the ``future`` version, you can disable auto-escaping with::
{% autoescape off %}
{% firstof var1 var2 var3 "<strong>fallback value</strong>" %}
{% endautoescape %}
Or if only some variables should be escaped, you can use::
{% firstof var1 var2|safe var3 "<strong>fallback value</strong>"|safe %}
.. templatetag:: for .. templatetag:: for
for for

View File

@ -160,6 +160,34 @@ Backwards incompatible changes in 1.6
Features deprecated in 1.6 Features deprecated in 1.6
========================== ==========================
Changes to :ttag:`cycle` and :ttag:`firstof`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The template system generally escapes all variables to avoid XSS attacks.
However, due to an accident of history, the :ttag:`cycle` and :ttag:`firstof`
tags render their arguments as-is.
Django 1.6 starts a process to correct this inconsistency. The ``future``
template library provides alternate implementations of :ttag:`cycle` and
:ttag:`firstof` that autoescape their inputs. If you're using these tags,
you're encourage to include the following line at the top of your templates to
enable the new behavior::
{% load cycle from future %}
or::
{% load firstof from future %}
The tags implementing the old behavior have been deprecated, and in Django
1.8, the old behavior will be replaced with the new behavior. To ensure
compatibility with future versions of Django, existing templates should be
modified to use the ``future`` versions.
If necessary, you can temporarily disable auto-escaping with
:func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %}
<autoescape>`.
``SEND_BROKEN_LINK_EMAILS`` setting ``SEND_BROKEN_LINK_EMAILS`` setting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -773,6 +773,11 @@ class Templates(TestCase):
'cycle23': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{{ abc }}{{ x }}{% endfor %}", {'values': [1,2,3,4]}, "a1b2c3a4"), 'cycle23': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{{ abc }}{{ x }}{% endfor %}", {'values': [1,2,3,4]}, "a1b2c3a4"),
'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'), 'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'),
'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"), 'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"),
'cycle25': ('{% cycle a as abc %}', {'a': '<'}, '<'),
'cycle26': ('{% load cycle from future %}{% cycle a b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '&lt;&gt;'),
'cycle27': ('{% load cycle from future %}{% autoescape off %}{% cycle a b as ab %}{% cycle ab %}{% endautoescape %}', {'a': '<', 'b': '>'}, '<>'),
'cycle28': ('{% load cycle from future %}{% cycle a|safe b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '<&gt;'),
### EXCEPTIONS ############################################################ ### EXCEPTIONS ############################################################
@ -804,7 +809,12 @@ class Templates(TestCase):
'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'), 'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'),
'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'), 'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'),
'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError), 'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError),
'firstof10': ('{% firstof a %}', {'a': '<'}, '<'), # Variables are NOT auto-escaped. 'firstof10': ('{% firstof a %}', {'a': '<'}, '<'),
'firstof11': ('{% load firstof from future %}{% firstof a b %}', {'a': '<', 'b': '>'}, '&lt;'),
'firstof12': ('{% load firstof from future %}{% firstof a b %}', {'a': '', 'b': '>'}, '&gt;'),
'firstof13': ('{% load firstof from future %}{% autoescape off %}{% firstof a %}{% endautoescape %}', {'a': '<'}, '<'),
'firstof14': ('{% load firstof from future %}{% firstof a|safe b %}', {'a': '<'}, '<'),
### FOR TAG ############################################################### ### FOR TAG ###############################################################
'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"), 'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"),