mirror of https://github.com/django/django.git
Fixed #25018 -- Changed simple_tag to apply conditional_escape() to its output.
This is a security hardening fix to help prevent XSS (and incorrect HTML) for the common use case of simple_tag. Thanks to Tim Graham for the review.
This commit is contained in:
parent
9ed82154bd
commit
aef2a0ec59
|
@ -4,6 +4,7 @@ from importlib import import_module
|
||||||
|
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.deprecation import RemovedInDjango20Warning
|
from django.utils.deprecation import RemovedInDjango20Warning
|
||||||
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.inspect import getargspec
|
from django.utils.inspect import getargspec
|
||||||
from django.utils.itercompat import is_iterable
|
from django.utils.itercompat import is_iterable
|
||||||
|
|
||||||
|
@ -201,6 +202,8 @@ class SimpleNode(TagHelperNode):
|
||||||
if self.target_var is not None:
|
if self.target_var is not None:
|
||||||
context[self.target_var] = output
|
context[self.target_var] = output
|
||||||
return ''
|
return ''
|
||||||
|
if context.autoescape:
|
||||||
|
output = conditional_escape(output)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -441,6 +441,22 @@ A few things to note about the ``simple_tag`` helper function:
|
||||||
* If the argument was a template variable, our function is passed the
|
* If the argument was a template variable, our function is passed the
|
||||||
current value of the variable, not the variable itself.
|
current value of the variable, not the variable itself.
|
||||||
|
|
||||||
|
Unlike other tag utilities, ``simple_tag`` passes its output through
|
||||||
|
:func:`~django.utils.html.conditional_escape` if the template context is in
|
||||||
|
autoescape mode, to ensure correct HTML and protect you from XSS
|
||||||
|
vulnerabilities.
|
||||||
|
|
||||||
|
If additional escaping is not desired, you will need to use
|
||||||
|
:func:`~django.utils.safestring.mark_safe` if you are absolutely sure that your
|
||||||
|
code does not contain XSS vulnerabilities. For building small HTML snippets,
|
||||||
|
use of :func:`~django.utils.html.format_html` instead of ``mark_safe()`` is
|
||||||
|
strongly recommended.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
Auto-escaping for ``simple_tag`` as described in the previous two paragraphs
|
||||||
|
was added.
|
||||||
|
|
||||||
If your template tag needs to access the current context, you can use the
|
If your template tag needs to access the current context, you can use the
|
||||||
``takes_context`` argument when registering your tag::
|
``takes_context`` argument when registering your tag::
|
||||||
|
|
||||||
|
@ -792,12 +808,16 @@ Ultimately, this decoupling of compilation and rendering results in an
|
||||||
efficient template system, because a template can render multiple contexts
|
efficient template system, because a template can render multiple contexts
|
||||||
without having to be parsed multiple times.
|
without having to be parsed multiple times.
|
||||||
|
|
||||||
|
.. _tags-auto-escaping:
|
||||||
|
|
||||||
Auto-escaping considerations
|
Auto-escaping considerations
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The output from template tags is **not** automatically run through the
|
The output from template tags is **not** automatically run through the
|
||||||
auto-escaping filters. However, there are still a couple of things you should
|
auto-escaping filters (with the exception of
|
||||||
keep in mind when writing a template tag.
|
:meth:`~django.template.Library.simple_tag` as described above). However, there
|
||||||
|
are still a couple of things you should keep in mind when writing a template
|
||||||
|
tag.
|
||||||
|
|
||||||
If the ``render()`` function of your template stores the result in a context
|
If the ``render()`` function of your template stores the result in a context
|
||||||
variable (rather than returning the result in a string), it should take care
|
variable (rather than returning the result in a string), it should take care
|
||||||
|
|
|
@ -684,6 +684,49 @@ define built-in libraries via the ``'builtins'`` key of :setting:`OPTIONS
|
||||||
<TEMPLATES-OPTIONS>` when defining a
|
<TEMPLATES-OPTIONS>` when defining a
|
||||||
:class:`~django.template.backends.django.DjangoTemplates` backend.
|
:class:`~django.template.backends.django.DjangoTemplates` backend.
|
||||||
|
|
||||||
|
.. _simple-tag-conditional-escape-fix:
|
||||||
|
|
||||||
|
``simple_tag`` now wraps tag output in ``conditional_escape``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In general, template tags do not autoescape their contents, and this behavior is
|
||||||
|
:ref:`documented <tags-auto-escaping>`. For tags like
|
||||||
|
:class:`~django.template.Library.inclusion_tag`, this is not a problem because
|
||||||
|
the included template will perform autoescaping. For
|
||||||
|
:class:`~django.template.Library.assignment_tag`, the output will be escaped
|
||||||
|
when it is used as a variable in the template.
|
||||||
|
|
||||||
|
For the intended use cases of :class:`~django.template.Library.simple_tag`,
|
||||||
|
however, it is very easy to end up with incorrect HTML and possibly an XSS
|
||||||
|
exploit. For example::
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def greeting(context):
|
||||||
|
return "Hello {0}!".format(context['request'].user.first_name)
|
||||||
|
|
||||||
|
In older versions of Django, this will be an XSS issue because
|
||||||
|
``user.first_name`` is not escaped.
|
||||||
|
|
||||||
|
In Django 1.9, this is fixed: if the template context has ``autoescape=True``
|
||||||
|
set (the default), then ``simple_tag`` will wrap the output of the tag function
|
||||||
|
with :func:`~django.utils.html.conditional_escape`.
|
||||||
|
|
||||||
|
To fix your ``simple_tag``\s, it is best to apply the following practices:
|
||||||
|
|
||||||
|
* Any code that generates HTML should use either the template system or
|
||||||
|
:func:`~django.utils.html.format_html`.
|
||||||
|
|
||||||
|
* If the output of a ``simple_tag`` needs escaping, use
|
||||||
|
:func:`~django.utils.html.escape` or
|
||||||
|
:func:`~django.utils.html.conditional_escape`.
|
||||||
|
|
||||||
|
* If you are absolutely certain that you are outputting HTML from a trusted
|
||||||
|
source (e.g. a CMS field that stores HTML entered by admins), you can mark it
|
||||||
|
as such using :func:`~django.utils.safestring.mark_safe`.
|
||||||
|
|
||||||
|
Tags that follow these rules will be correct and safe whether they are run on
|
||||||
|
Django 1.9+ or earlier.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -2434,7 +2434,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
|
||||||
expected_link = reverse('admin:%s_modelwithstringprimarykey_history' %
|
expected_link = reverse('admin:%s_modelwithstringprimarykey_history' %
|
||||||
ModelWithStringPrimaryKey._meta.app_label,
|
ModelWithStringPrimaryKey._meta.app_label,
|
||||||
args=(quote(self.pk),))
|
args=(quote(self.pk),))
|
||||||
self.assertContains(response, '<a href="%s" class="historylink"' % expected_link)
|
self.assertContains(response, '<a href="%s" class="historylink"' % escape(expected_link))
|
||||||
|
|
||||||
def test_redirect_on_add_view_continue_button(self):
|
def test_redirect_on_add_view_continue_button(self):
|
||||||
"""As soon as an object is added using "Save and continue editing"
|
"""As soon as an object is added using "Save and continue editing"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import warnings
|
||||||
from django import template
|
from django import template
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
from django.utils.html import escape, format_html
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
@ -109,6 +110,24 @@ def simple_tag_without_context_parameter(arg):
|
||||||
simple_tag_without_context_parameter.anything = "Expected simple_tag_without_context_parameter __dict__"
|
simple_tag_without_context_parameter.anything = "Expected simple_tag_without_context_parameter __dict__"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def escape_naive(context):
|
||||||
|
"""A tag that doesn't even think about escaping issues"""
|
||||||
|
return "Hello {0}!".format(context['name'])
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def escape_explicit(context):
|
||||||
|
"""A tag that uses escape explicitly"""
|
||||||
|
return escape("Hello {0}!".format(context['name']))
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def escape_format_html(context):
|
||||||
|
"""A tag that uses format_html"""
|
||||||
|
return format_html("Hello {0}!", context['name'])
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def current_app(context):
|
def current_app(context):
|
||||||
return "%s" % context.current_app
|
return "%s" % context.current_app
|
||||||
|
|
|
@ -104,6 +104,28 @@ class SimpleTagTests(TagTestCase):
|
||||||
with self.assertRaisesMessage(TemplateSyntaxError, entry[0]):
|
with self.assertRaisesMessage(TemplateSyntaxError, entry[0]):
|
||||||
self.engine.from_string("%s as var %%}" % entry[1][0:-2])
|
self.engine.from_string("%s as var %%}" % entry[1][0:-2])
|
||||||
|
|
||||||
|
def test_simple_tag_escaping_autoescape_off(self):
|
||||||
|
c = Context({'name': "Jack & Jill"}, autoescape=False)
|
||||||
|
t = self.engine.from_string("{% load custom %}{% escape_naive %}")
|
||||||
|
self.assertEqual(t.render(c), "Hello Jack & Jill!")
|
||||||
|
|
||||||
|
def test_simple_tag_naive_escaping(self):
|
||||||
|
c = Context({'name': "Jack & Jill"})
|
||||||
|
t = self.engine.from_string("{% load custom %}{% escape_naive %}")
|
||||||
|
self.assertEqual(t.render(c), "Hello Jack & Jill!")
|
||||||
|
|
||||||
|
def test_simple_tag_explicit_escaping(self):
|
||||||
|
# Check we don't double escape
|
||||||
|
c = Context({'name': "Jack & Jill"})
|
||||||
|
t = self.engine.from_string("{% load custom %}{% escape_explicit %}")
|
||||||
|
self.assertEqual(t.render(c), "Hello Jack & Jill!")
|
||||||
|
|
||||||
|
def test_simple_tag_format_html_escaping(self):
|
||||||
|
# Check we don't double escape
|
||||||
|
c = Context({'name': "Jack & Jill"})
|
||||||
|
t = self.engine.from_string("{% load custom %}{% escape_format_html %}")
|
||||||
|
self.assertEqual(t.render(c), "Hello Jack & Jill!")
|
||||||
|
|
||||||
def test_simple_tag_registration(self):
|
def test_simple_tag_registration(self):
|
||||||
# Test that the decorators preserve the decorated function's docstring, name and attributes.
|
# Test that the decorators preserve the decorated function's docstring, name and attributes.
|
||||||
self.verify_tag(custom.no_params, 'no_params')
|
self.verify_tag(custom.no_params, 'no_params')
|
||||||
|
|
Loading…
Reference in New Issue