Skip to content

Commit d16e4e1

Browse files
mxsashatimgraham
authored andcommitted
[1.8.x] Fixed #24464 -- Made built-in HTML template filter functions escape their input by default.
This may cause some backwards compatibility issues, but may also resolve security issues in third party projects that fail to heed warnings in our documentation. Thanks Markus Holtermann for help with tests and docs. Backport of fa350e2 from master
1 parent 3a0fe94 commit d16e4e1

File tree

10 files changed

+174
-24
lines changed

10 files changed

+174
-24
lines changed

django/template/defaultfilters.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def iriencode(value):
191191

192192
@register.filter(is_safe=True, needs_autoescape=True)
193193
@stringfilter
194-
def linenumbers(value, autoescape=None):
194+
def linenumbers(value, autoescape=True):
195195
"""Displays text with line numbers."""
196196
lines = value.split('\n')
197197
# Find the maximum width of the line count, for use with zero padding
@@ -353,14 +353,14 @@ def urlencode(value, safe=None):
353353

354354
@register.filter(is_safe=True, needs_autoescape=True)
355355
@stringfilter
356-
def urlize(value, autoescape=None):
356+
def urlize(value, autoescape=True):
357357
"""Converts URLs in plain text into clickable links."""
358358
return mark_safe(_urlize(value, nofollow=True, autoescape=autoescape))
359359

360360

361361
@register.filter(is_safe=True, needs_autoescape=True)
362362
@stringfilter
363-
def urlizetrunc(value, limit, autoescape=None):
363+
def urlizetrunc(value, limit, autoescape=True):
364364
"""
365365
Converts URLs into clickable links, truncating URLs to the given character
366366
limit, and adding 'rel=nofollow' attribute to discourage spamming.
@@ -457,7 +457,7 @@ def force_escape(value):
457457

458458
@register.filter("linebreaks", is_safe=True, needs_autoescape=True)
459459
@stringfilter
460-
def linebreaks_filter(value, autoescape=None):
460+
def linebreaks_filter(value, autoescape=True):
461461
"""
462462
Replaces line breaks in plain text with appropriate HTML; a single
463463
newline becomes an HTML line break (``<br />``) and a new line
@@ -469,7 +469,7 @@ def linebreaks_filter(value, autoescape=None):
469469

470470
@register.filter(is_safe=True, needs_autoescape=True)
471471
@stringfilter
472-
def linebreaksbr(value, autoescape=None):
472+
def linebreaksbr(value, autoescape=True):
473473
"""
474474
Converts all newlines in a piece of plain text to HTML line breaks
475475
(``<br />``).
@@ -552,7 +552,7 @@ def first(value):
552552

553553

554554
@register.filter(is_safe=True, needs_autoescape=True)
555-
def join(value, arg, autoescape=None):
555+
def join(value, arg, autoescape=True):
556556
"""
557557
Joins a list with a string, like Python's ``str.join(list)``.
558558
"""
@@ -622,7 +622,7 @@ def slice_filter(value, arg):
622622

623623

624624
@register.filter(is_safe=True, needs_autoescape=True)
625-
def unordered_list(value, autoescape=None):
625+
def unordered_list(value, autoescape=True):
626626
"""
627627
Recursively takes a self-nested list and returns an HTML unordered list --
628628
WITHOUT opening and closing <ul> tags.

docs/howto/custom-template-tags.txt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ Template filter code falls into one of two situations:
281281
(If you don't specify this flag, it defaults to ``False``). This flag tells
282282
Django that your filter function wants to be passed an extra keyword
283283
argument, called ``autoescape``, that is ``True`` if auto-escaping is in
284-
effect and ``False`` otherwise.
284+
effect and ``False`` otherwise. It is recommended to set the default of the
285+
``autoescape`` parameter to ``True``, so that if you call the function
286+
from Python code it will have escaping enabled by default.
285287

286288
For example, let's write a filter that emphasizes the first character of
287289
a string::
@@ -293,7 +295,7 @@ Template filter code falls into one of two situations:
293295
register = template.Library()
294296

295297
@register.filter(needs_autoescape=True)
296-
def initial_letter_filter(text, autoescape=None):
298+
def initial_letter_filter(text, autoescape=True):
297299
first, other = text[0], text[1:]
298300
if autoescape:
299301
esc = conditional_escape
@@ -323,19 +325,28 @@ Template filter code falls into one of two situations:
323325

324326
.. warning:: Avoiding XSS vulnerabilities when reusing built-in filters
325327

326-
Be careful when reusing Django's built-in filters. You'll need to pass
327-
``autoescape=True`` to the filter in order to get the proper autoescaping
328-
behavior and avoid a cross-site script vulnerability.
328+
.. versionchanged:: 1.8
329+
330+
Django's built-in filters have ``autoescape=True`` by default in order to
331+
get the proper autoescaping behavior and avoid a cross-site script
332+
vulnerability.
333+
334+
In older versions of Django, be careful when reusing Django's built-in
335+
filters as ``autoescape`` defaults to ``None``. You'll need to pass
336+
``autoescape=True`` to get autoescaping.
329337

330338
For example, if you wanted to write a custom filter called
331339
``urlize_and_linebreaks`` that combined the :tfilter:`urlize` and
332340
:tfilter:`linebreaksbr` filters, the filter would look like::
333341

334342
from django.template.defaultfilters import linebreaksbr, urlize
335343

336-
@register.filter
337-
def urlize_and_linebreaks(text):
338-
return linebreaksbr(urlize(text, autoescape=True), autoescape=True)
344+
@register.filter(needs_autoescape=True)
345+
def urlize_and_linebreaks(text, autoescape=True):
346+
return linebreaksbr(
347+
urlize(text, autoescape=autoescape),
348+
autoescape=autoescape
349+
)
339350

340351
Then:
341352

docs/releases/1.8.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,26 @@ those writing third-party backends in updating their code:
10111011
now takes a second argument named ``obj_id`` which is the serialized
10121012
identifier used to retrieve the object before deletion.
10131013

1014+
Default autoescaping of functions in ``django.template.defaultfilters``
1015+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1016+
1017+
In order to make built-in template filters that output HTML "safe by default"
1018+
when calling them in Python code, the following functions in
1019+
``django.template.defaultfilters`` have been changed to automatically escape
1020+
their input value:
1021+
1022+
* ``join``
1023+
* ``linebreaksbr``
1024+
* ``linebreaks_filter``
1025+
* ``linenumbers``
1026+
* ``unordered_list``
1027+
* ``urlize``
1028+
* ``urlizetrunc``
1029+
1030+
You can revert to the old behavior by specifying ``autoescape=False`` if you
1031+
are passing trusted content. This change doesn't have any effect when using
1032+
the corresponding filters in templates.
1033+
10141034
Miscellaneous
10151035
~~~~~~~~~~~~~
10161036

tests/template_tests/filter_tests/test_join.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,15 @@ class FunctionTests(SimpleTestCase):
5454

5555
def test_list(self):
5656
self.assertEqual(join([0, 1, 2], 'glue'), '0glue1glue2')
57+
58+
def test_autoescape(self):
59+
self.assertEqual(
60+
join(['<a>', '<img>', '</a>'], '<br>'),
61+
'&lt;a&gt;&lt;br&gt;&lt;img&gt;&lt;br&gt;&lt;/a&gt;',
62+
)
63+
64+
def test_autoescape_off(self):
65+
self.assertEqual(
66+
join(['<a>', '<img>', '</a>'], '<br>', autoescape=False),
67+
'<a>&lt;br&gt;<img>&lt;br&gt;</a>',
68+
)

tests/template_tests/filter_tests/test_linebreaks.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,15 @@ def test_carriage_newline(self):
3939

4040
def test_non_string_input(self):
4141
self.assertEqual(linebreaks_filter(123), '<p>123</p>')
42+
43+
def test_autoescape(self):
44+
self.assertEqual(
45+
linebreaks_filter('foo\n<a>bar</a>\nbuz'),
46+
'<p>foo<br />&lt;a&gt;bar&lt;/a&gt;<br />buz</p>',
47+
)
48+
49+
def test_autoescape_off(self):
50+
self.assertEqual(
51+
linebreaks_filter('foo\n<a>bar</a>\nbuz', autoescape=False),
52+
'<p>foo<br /><a>bar</a><br />buz</p>',
53+
)

tests/template_tests/filter_tests/test_linebreaksbr.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,15 @@ def test_carriage_newline(self):
3636

3737
def test_non_string_input(self):
3838
self.assertEqual(linebreaksbr(123), '123')
39+
40+
def test_autoescape(self):
41+
self.assertEqual(
42+
linebreaksbr('foo\n<a>bar</a>\nbuz'),
43+
'foo<br />&lt;a&gt;bar&lt;/a&gt;<br />buz',
44+
)
45+
46+
def test_autoescape_off(self):
47+
self.assertEqual(
48+
linebreaksbr('foo\n<a>bar</a>\nbuz', autoescape=False),
49+
'foo<br /><a>bar</a><br />buz',
50+
)

tests/template_tests/filter_tests/test_linenumbers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,15 @@ def test_linenumbers2(self):
4444

4545
def test_non_string_input(self):
4646
self.assertEqual(linenumbers(123), '1. 123')
47+
48+
def test_autoescape(self):
49+
self.assertEqual(
50+
linenumbers('foo\n<a>bar</a>\nbuz'),
51+
'1. foo\n2. &lt;a&gt;bar&lt;/a&gt;\n3. buz',
52+
)
53+
54+
def test_autoescape_off(self):
55+
self.assertEqual(
56+
linenumbers('foo\n<a>bar</a>\nbuz', autoescape=False),
57+
'1. foo\n2. <a>bar</a>\n3. buz'
58+
)

tests/template_tests/filter_tests/test_unordered_list.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ def test_nested_multiple2(self):
9999
'\n\t\t<li>Illinois</li>\n\t</ul>\n\t</li>',
100100
)
101101

102+
def test_autoescape(self):
103+
self.assertEqual(
104+
unordered_list(['<a>item 1</a>', 'item 2']),
105+
'\t<li>&lt;a&gt;item 1&lt;/a&gt;</li>\n\t<li>item 2</li>',
106+
)
107+
108+
def test_autoescape_off(self):
109+
self.assertEqual(
110+
unordered_list(['<a>item 1</a>', 'item 2'], autoescape=False),
111+
'\t<li><a>item 1</a></li>\n\t<li>item 2</li>',
112+
)
113+
102114
def test_ulitem(self):
103115
@python_2_unicode_compatible
104116
class ULItem(object):
@@ -110,13 +122,48 @@ def __str__(self):
110122

111123
a = ULItem('a')
112124
b = ULItem('b')
113-
self.assertEqual(unordered_list([a, b]), '\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>')
125+
c = ULItem('<a>c</a>')
126+
self.assertEqual(
127+
unordered_list([a, b, c]),
128+
'\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>\n\t<li>ulitem-&lt;a&gt;c&lt;/a&gt;</li>',
129+
)
130+
131+
def item_generator():
132+
yield a
133+
yield b
134+
yield c
135+
136+
self.assertEqual(
137+
unordered_list(item_generator()),
138+
'\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>\n\t<li>ulitem-&lt;a&gt;c&lt;/a&gt;</li>',
139+
)
140+
141+
def test_ulitem_autoescape_off(self):
142+
@python_2_unicode_compatible
143+
class ULItem(object):
144+
def __init__(self, title):
145+
self.title = title
146+
147+
def __str__(self):
148+
return 'ulitem-%s' % str(self.title)
149+
150+
a = ULItem('a')
151+
b = ULItem('b')
152+
c = ULItem('<a>c</a>')
153+
self.assertEqual(
154+
unordered_list([a, b, c], autoescape=False),
155+
'\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>\n\t<li>ulitem-<a>c</a></li>',
156+
)
114157

115158
def item_generator():
116159
yield a
117160
yield b
161+
yield c
118162

119-
self.assertEqual(unordered_list(item_generator()), '\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>')
163+
self.assertEqual(
164+
unordered_list(item_generator(), autoescape=False),
165+
'\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>\n\t<li>ulitem-<a>c</a></li>',
166+
)
120167

121168
@ignore_warnings(category=RemovedInDjango20Warning)
122169
def test_legacy(self):

tests/template_tests/filter_tests/test_urlize.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,27 +259,27 @@ def test_quotation_marks(self):
259259
#20364 - Check urlize correctly include quotation marks in links
260260
"""
261261
self.assertEqual(
262-
urlize('before "hi@example.com" afterwards'),
262+
urlize('before "hi@example.com" afterwards', autoescape=False),
263263
'before "<a href="mailto:hi@example.com">hi@example.com</a>" afterwards',
264264
)
265265
self.assertEqual(
266-
urlize('before hi@example.com" afterwards'),
266+
urlize('before hi@example.com" afterwards', autoescape=False),
267267
'before <a href="mailto:hi@example.com">hi@example.com</a>" afterwards',
268268
)
269269
self.assertEqual(
270-
urlize('before "hi@example.com afterwards'),
270+
urlize('before "hi@example.com afterwards', autoescape=False),
271271
'before "<a href="mailto:hi@example.com">hi@example.com</a> afterwards',
272272
)
273273
self.assertEqual(
274-
urlize('before \'hi@example.com\' afterwards'),
274+
urlize('before \'hi@example.com\' afterwards', autoescape=False),
275275
'before \'<a href="mailto:hi@example.com">hi@example.com</a>\' afterwards',
276276
)
277277
self.assertEqual(
278-
urlize('before hi@example.com\' afterwards'),
278+
urlize('before hi@example.com\' afterwards', autoescape=False),
279279
'before <a href="mailto:hi@example.com">hi@example.com</a>\' afterwards',
280280
)
281281
self.assertEqual(
282-
urlize('before \'hi@example.com afterwards'),
282+
urlize('before \'hi@example.com afterwards', autoescape=False),
283283
'before \'<a href="mailto:hi@example.com">hi@example.com</a> afterwards',
284284
)
285285

@@ -288,7 +288,7 @@ def test_quote_commas(self):
288288
#20364 - Check urlize copes with commas following URLs in quotes
289289
"""
290290
self.assertEqual(
291-
urlize('Email us at "hi@example.com", or phone us at +xx.yy'),
291+
urlize('Email us at "hi@example.com", or phone us at +xx.yy', autoescape=False),
292292
'Email us at "<a href="mailto:hi@example.com">hi@example.com</a>", or phone us at +xx.yy',
293293
)
294294

@@ -316,3 +316,15 @@ def test_exclamation_marks(self):
316316

317317
def test_non_string_input(self):
318318
self.assertEqual(urlize(123), '123')
319+
320+
def test_autoescape(self):
321+
self.assertEqual(
322+
urlize('foo<a href=" google.com ">bar</a>buz'),
323+
'foo&lt;a href=&quot; <a href="http://google.com" rel="nofollow">google.com</a> &quot;&gt;bar&lt;/a&gt;buz',
324+
)
325+
326+
def test_autoescape_off(self):
327+
self.assertEqual(
328+
urlize('foo<a href=" google.com ">bar</a>buz', autoescape=False),
329+
'foo<a href=" <a href="http://google.com" rel="nofollow">google.com</a> ">bar</a>buz',
330+
)

tests/template_tests/filter_tests/test_urlizetrunc.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,15 @@ def test_query_string(self):
7878

7979
def test_non_string_input(self):
8080
self.assertEqual(urlizetrunc(123, 1), '123')
81+
82+
def test_autoescape(self):
83+
self.assertEqual(
84+
urlizetrunc('foo<a href=" google.com ">bar</a>buz', 10),
85+
'foo&lt;a href=&quot; <a href="http://google.com" rel="nofollow">google.com</a> &quot;&gt;bar&lt;/a&gt;buz',
86+
)
87+
88+
def test_autoescape_off(self):
89+
self.assertEqual(
90+
urlizetrunc('foo<a href=" google.com ">bar</a>buz', 9, autoescape=False),
91+
'foo<a href=" <a href="http://google.com" rel="nofollow">google...</a> ">bar</a>buz',
92+
)

0 commit comments

Comments
 (0)