diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 57a4a7f1f9..fe71459358 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -191,7 +191,7 @@ def iriencode(value):
@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
-def linenumbers(value, autoescape=None):
+def linenumbers(value, autoescape=True):
"""Displays text with line numbers."""
lines = value.split('\n')
# Find the maximum width of the line count, for use with zero padding
@@ -353,14 +353,14 @@ def urlencode(value, safe=None):
@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
-def urlize(value, autoescape=None):
+def urlize(value, autoescape=True):
"""Converts URLs in plain text into clickable links."""
return mark_safe(_urlize(value, nofollow=True, autoescape=autoescape))
@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
-def urlizetrunc(value, limit, autoescape=None):
+def urlizetrunc(value, limit, autoescape=True):
"""
Converts URLs into clickable links, truncating URLs to the given character
limit, and adding 'rel=nofollow' attribute to discourage spamming.
@@ -457,7 +457,7 @@ def force_escape(value):
@register.filter("linebreaks", is_safe=True, needs_autoescape=True)
@stringfilter
-def linebreaks_filter(value, autoescape=None):
+def linebreaks_filter(value, autoescape=True):
"""
Replaces line breaks in plain text with appropriate HTML; a single
newline becomes an HTML line break (``
``) and a new line
@@ -469,7 +469,7 @@ def linebreaks_filter(value, autoescape=None):
@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
-def linebreaksbr(value, autoescape=None):
+def linebreaksbr(value, autoescape=True):
"""
Converts all newlines in a piece of plain text to HTML line breaks
(``
``).
@@ -552,7 +552,7 @@ def first(value):
@register.filter(is_safe=True, needs_autoescape=True)
-def join(value, arg, autoescape=None):
+def join(value, arg, autoescape=True):
"""
Joins a list with a string, like Python's ``str.join(list)``.
"""
@@ -622,7 +622,7 @@ def slice_filter(value, arg):
@register.filter(is_safe=True, needs_autoescape=True)
-def unordered_list(value, autoescape=None):
+def unordered_list(value, autoescape=True):
"""
Recursively takes a self-nested list and returns an HTML unordered list --
WITHOUT opening and closing
tags.
diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt
index 3517d703fb..cf1ca4036f 100644
--- a/docs/howto/custom-template-tags.txt
+++ b/docs/howto/custom-template-tags.txt
@@ -281,7 +281,9 @@ Template filter code falls into one of two situations:
(If you don't specify this flag, it defaults to ``False``). This flag tells
Django that your filter function wants to be passed an extra keyword
argument, called ``autoescape``, that is ``True`` if auto-escaping is in
- effect and ``False`` otherwise.
+ effect and ``False`` otherwise. It is recommended to set the default of the
+ ``autoescape`` parameter to ``True``, so that if you call the function
+ from Python code it will have escaping enabled by default.
For example, let's write a filter that emphasizes the first character of
a string::
@@ -293,7 +295,7 @@ Template filter code falls into one of two situations:
register = template.Library()
@register.filter(needs_autoescape=True)
- def initial_letter_filter(text, autoescape=None):
+ def initial_letter_filter(text, autoescape=True):
first, other = text[0], text[1:]
if autoescape:
esc = conditional_escape
@@ -323,9 +325,15 @@ Template filter code falls into one of two situations:
.. warning:: Avoiding XSS vulnerabilities when reusing built-in filters
- Be careful when reusing Django's built-in filters. You'll need to pass
- ``autoescape=True`` to the filter in order to get the proper autoescaping
- behavior and avoid a cross-site script vulnerability.
+ .. versionchanged:: 1.8
+
+ Django's built-in filters have ``autoescape=True`` by default in order to
+ get the proper autoescaping behavior and avoid a cross-site script
+ vulnerability.
+
+ In older versions of Django, be careful when reusing Django's built-in
+ filters as ``autoescape`` defaults to ``None``. You'll need to pass
+ ``autoescape=True`` to get autoescaping.
For example, if you wanted to write a custom filter called
``urlize_and_linebreaks`` that combined the :tfilter:`urlize` and
@@ -333,9 +341,12 @@ Template filter code falls into one of two situations:
from django.template.defaultfilters import linebreaksbr, urlize
- @register.filter
- def urlize_and_linebreaks(text):
- return linebreaksbr(urlize(text, autoescape=True), autoescape=True)
+ @register.filter(needs_autoescape=True)
+ def urlize_and_linebreaks(text, autoescape=True):
+ return linebreaksbr(
+ urlize(text, autoescape=autoescape),
+ autoescape=autoescape
+ )
Then:
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index a88b406a32..b7593c85ce 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -1011,6 +1011,26 @@ those writing third-party backends in updating their code:
now takes a second argument named ``obj_id`` which is the serialized
identifier used to retrieve the object before deletion.
+Default autoescaping of functions in ``django.template.defaultfilters``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to make built-in template filters that output HTML "safe by default"
+when calling them in Python code, the following functions in
+``django.template.defaultfilters`` have been changed to automatically escape
+their input value:
+
+* ``join``
+* ``linebreaksbr``
+* ``linebreaks_filter``
+* ``linenumbers``
+* ``unordered_list``
+* ``urlize``
+* ``urlizetrunc``
+
+You can revert to the old behavior by specifying ``autoescape=False`` if you
+are passing trusted content. This change doesn't have any effect when using
+the corresponding filters in templates.
+
Miscellaneous
~~~~~~~~~~~~~
diff --git a/tests/template_tests/filter_tests/test_join.py b/tests/template_tests/filter_tests/test_join.py
index 1a5193824b..12d2f7257e 100644
--- a/tests/template_tests/filter_tests/test_join.py
+++ b/tests/template_tests/filter_tests/test_join.py
@@ -54,3 +54,15 @@ class FunctionTests(SimpleTestCase):
def test_list(self):
self.assertEqual(join([0, 1, 2], 'glue'), '0glue1glue2')
+
+ def test_autoescape(self):
+ self.assertEqual(
+ join(['', '', ''], '
'),
+ '<a><br><img><br></a>',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ join(['', '', ''], '
', autoescape=False),
+ '<br><br>',
+ )
diff --git a/tests/template_tests/filter_tests/test_linebreaks.py b/tests/template_tests/filter_tests/test_linebreaks.py
index 041bfb7617..d895a067bb 100644
--- a/tests/template_tests/filter_tests/test_linebreaks.py
+++ b/tests/template_tests/filter_tests/test_linebreaks.py
@@ -39,3 +39,15 @@ class FunctionTests(SimpleTestCase):
def test_non_string_input(self):
self.assertEqual(linebreaks_filter(123), '123
')
+
+ def test_autoescape(self):
+ self.assertEqual(
+ linebreaks_filter('foo\nbar\nbuz'),
+ 'foo
<a>bar</a>
buz
',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ linebreaks_filter('foo\nbar\nbuz', autoescape=False),
+ 'foo
bar
buz
',
+ )
diff --git a/tests/template_tests/filter_tests/test_linebreaksbr.py b/tests/template_tests/filter_tests/test_linebreaksbr.py
index 0564402fa8..c702ba777f 100644
--- a/tests/template_tests/filter_tests/test_linebreaksbr.py
+++ b/tests/template_tests/filter_tests/test_linebreaksbr.py
@@ -36,3 +36,15 @@ class FunctionTests(SimpleTestCase):
def test_non_string_input(self):
self.assertEqual(linebreaksbr(123), '123')
+
+ def test_autoescape(self):
+ self.assertEqual(
+ linebreaksbr('foo\nbar\nbuz'),
+ 'foo
<a>bar</a>
buz',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ linebreaksbr('foo\nbar\nbuz', autoescape=False),
+ 'foo
bar
buz',
+ )
diff --git a/tests/template_tests/filter_tests/test_linenumbers.py b/tests/template_tests/filter_tests/test_linenumbers.py
index d8c18b84d5..36ec8de0d5 100644
--- a/tests/template_tests/filter_tests/test_linenumbers.py
+++ b/tests/template_tests/filter_tests/test_linenumbers.py
@@ -44,3 +44,15 @@ class FunctionTests(SimpleTestCase):
def test_non_string_input(self):
self.assertEqual(linenumbers(123), '1. 123')
+
+ def test_autoescape(self):
+ self.assertEqual(
+ linenumbers('foo\nbar\nbuz'),
+ '1. foo\n2. <a>bar</a>\n3. buz',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ linenumbers('foo\nbar\nbuz', autoescape=False),
+ '1. foo\n2. bar\n3. buz'
+ )
diff --git a/tests/template_tests/filter_tests/test_unordered_list.py b/tests/template_tests/filter_tests/test_unordered_list.py
index 59d677b59a..815d488347 100644
--- a/tests/template_tests/filter_tests/test_unordered_list.py
+++ b/tests/template_tests/filter_tests/test_unordered_list.py
@@ -99,6 +99,18 @@ class FunctionTests(SimpleTestCase):
'\n\t\t- Illinois
\n\t
\n\t',
)
+ def test_autoescape(self):
+ self.assertEqual(
+ unordered_list(['item 1', 'item 2']),
+ '\t<a>item 1</a>\n\titem 2',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ unordered_list(['item 1', 'item 2'], autoescape=False),
+ '\titem 1\n\titem 2',
+ )
+
def test_ulitem(self):
@python_2_unicode_compatible
class ULItem(object):
@@ -110,13 +122,48 @@ class FunctionTests(SimpleTestCase):
a = ULItem('a')
b = ULItem('b')
- self.assertEqual(unordered_list([a, b]), '\tulitem-a\n\tulitem-b')
+ c = ULItem('c')
+ self.assertEqual(
+ unordered_list([a, b, c]),
+ '\tulitem-a\n\tulitem-b\n\tulitem-<a>c</a>',
+ )
def item_generator():
yield a
yield b
+ yield c
- self.assertEqual(unordered_list(item_generator()), '\tulitem-a\n\tulitem-b')
+ self.assertEqual(
+ unordered_list(item_generator()),
+ '\tulitem-a\n\tulitem-b\n\tulitem-<a>c</a>',
+ )
+
+ def test_ulitem_autoescape_off(self):
+ @python_2_unicode_compatible
+ class ULItem(object):
+ def __init__(self, title):
+ self.title = title
+
+ def __str__(self):
+ return 'ulitem-%s' % str(self.title)
+
+ a = ULItem('a')
+ b = ULItem('b')
+ c = ULItem('c')
+ self.assertEqual(
+ unordered_list([a, b, c], autoescape=False),
+ '\tulitem-a\n\tulitem-b\n\tulitem-c',
+ )
+
+ def item_generator():
+ yield a
+ yield b
+ yield c
+
+ self.assertEqual(
+ unordered_list(item_generator(), autoescape=False),
+ '\tulitem-a\n\tulitem-b\n\tulitem-c',
+ )
@ignore_warnings(category=RemovedInDjango20Warning)
def test_legacy(self):
diff --git a/tests/template_tests/filter_tests/test_urlize.py b/tests/template_tests/filter_tests/test_urlize.py
index 1c55d64751..ee6744e6cb 100644
--- a/tests/template_tests/filter_tests/test_urlize.py
+++ b/tests/template_tests/filter_tests/test_urlize.py
@@ -259,27 +259,27 @@ class FunctionTests(SimpleTestCase):
#20364 - Check urlize correctly include quotation marks in links
"""
self.assertEqual(
- urlize('before "hi@example.com" afterwards'),
+ urlize('before "hi@example.com" afterwards', autoescape=False),
'before "hi@example.com" afterwards',
)
self.assertEqual(
- urlize('before hi@example.com" afterwards'),
+ urlize('before hi@example.com" afterwards', autoescape=False),
'before hi@example.com" afterwards',
)
self.assertEqual(
- urlize('before "hi@example.com afterwards'),
+ urlize('before "hi@example.com afterwards', autoescape=False),
'before "hi@example.com afterwards',
)
self.assertEqual(
- urlize('before \'hi@example.com\' afterwards'),
+ urlize('before \'hi@example.com\' afterwards', autoescape=False),
'before \'hi@example.com\' afterwards',
)
self.assertEqual(
- urlize('before hi@example.com\' afterwards'),
+ urlize('before hi@example.com\' afterwards', autoescape=False),
'before hi@example.com\' afterwards',
)
self.assertEqual(
- urlize('before \'hi@example.com afterwards'),
+ urlize('before \'hi@example.com afterwards', autoescape=False),
'before \'hi@example.com afterwards',
)
@@ -288,7 +288,7 @@ class FunctionTests(SimpleTestCase):
#20364 - Check urlize copes with commas following URLs in quotes
"""
self.assertEqual(
- urlize('Email us at "hi@example.com", or phone us at +xx.yy'),
+ urlize('Email us at "hi@example.com", or phone us at +xx.yy', autoescape=False),
'Email us at "hi@example.com", or phone us at +xx.yy',
)
@@ -316,3 +316,15 @@ class FunctionTests(SimpleTestCase):
def test_non_string_input(self):
self.assertEqual(urlize(123), '123')
+
+ def test_autoescape(self):
+ self.assertEqual(
+ urlize('foobarbuz'),
+ 'foo<a href=" google.com ">bar</a>buz',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ urlize('foobarbuz', autoescape=False),
+ 'foogoogle.com ">barbuz',
+ )
diff --git a/tests/template_tests/filter_tests/test_urlizetrunc.py b/tests/template_tests/filter_tests/test_urlizetrunc.py
index 9530128369..2b4b18e1f5 100644
--- a/tests/template_tests/filter_tests/test_urlizetrunc.py
+++ b/tests/template_tests/filter_tests/test_urlizetrunc.py
@@ -78,3 +78,15 @@ class FunctionTests(SimpleTestCase):
def test_non_string_input(self):
self.assertEqual(urlizetrunc(123, 1), '123')
+
+ def test_autoescape(self):
+ self.assertEqual(
+ urlizetrunc('foobarbuz', 10),
+ 'foo<a href=" google.com ">bar</a>buz',
+ )
+
+ def test_autoescape_off(self):
+ self.assertEqual(
+ urlizetrunc('foobarbuz', 9, autoescape=False),
+ 'foogoogle... ">barbuz',
+ )