1
2
3
4
5
6
7
8
9
10
11
12
13
14 from __future__ import unicode_literals
15
16 import doctest
17 import io
18 import sys
19 import unittest
20
21 try:
22 from babel.support import LazyProxy
23 except ImportError:
24 LazyProxy = None
25
26 from trac.core import TracError
27 from trac.util import html
28 from trac.util.html import (
29 Element, FormTokenInjector, Fragment, HTML, Markup, TracHTMLSanitizer,
30 escape, find_element, genshi, html_attribute, is_safe_origin, plaintext,
31 tag, to_fragment, xml
32 )
33 from trac.util.translation import gettext, tgettext
37
39 self.assertEqual(Markup('<b class="em"ph"">"1 < 2"</b>'),
40 escape(tag.b('"1 < 2"', class_='em"ph"')))
41 self.assertEqual(Markup('<b class="em"ph"">"1 < 2"</b>'),
42 escape(tag.b('"1 < 2"', class_='em"ph"'),
43 quotes=False))
44
46 self.assertEqual(Markup('<b class="em"ph"">"1 < 2"</b>'),
47 escape(tag(tag.b('"1 < 2"', class_='em"ph"'))))
48 self.assertEqual(Markup('<b class="em"ph"">"1 < 2"</b>'),
49 escape(tag(tag.b('"1 < 2"', class_='em"ph"')),
50 quotes=False))
51
52 @unittest.skipUnless(LazyProxy, 'Babel unavailable')
54 lazyproxy = gettext('Back to %(parent)s', parent='a&b<c>d"e\'f')
55 self.assertEqual(Markup('Back to a&b<c>d"e\'f'),
56 escape(lazyproxy))
57 self.assertEqual(Markup('Back to a&b<c>d"e\'f'),
58 escape(lazyproxy, quotes=False))
59
60 @unittest.skipUnless(LazyProxy, 'Babel unavailable')
62 lazyproxy = tgettext('Back to %(parent)s',
63 parent=tag.a('a&b<c>d"e\'f',
64 href='/a&b<c>d"e\'f'))
65 self.assertEqual(Markup('Back to <a '
66 'href="/a&b<c>d"e\'f">'
67 'a&b<c>d"e\'f</a>'),
68 escape(lazyproxy))
69 self.assertEqual(Markup('Back to <a '
70 'href="/a&b<c>d"e\'f">'
71 'a&b<c>d"e\'f</a>'),
72 escape(lazyproxy, quotes=False))
73
76
78 self.assertEqual('async', html_attribute('async', True))
79 self.assertEqual(None, html_attribute('async', False))
80 self.assertEqual(None, html_attribute('async', None))
81
83 self.assertEqual('yes', html_attribute('translate', True))
84 self.assertEqual('no', html_attribute('translate', False))
85 self.assertEqual('no', html_attribute('translate', None))
86
88 self.assertEqual('on', html_attribute('autocomplete', True))
89 self.assertEqual('off', html_attribute('autocomplete', False))
90 self.assertEqual('off', html_attribute('autocomplete', None))
91
93 self.assertEqual('true', html_attribute('spellcheck', True))
94 self.assertEqual('false', html_attribute('spellcheck', False))
95 self.assertEqual('false', html_attribute('spellcheck', None))
96
98 self.assertEqual('https://trac.edgewall.org',
99 html_attribute('src', 'https://trac.edgewall.org'))
100 self.assertEqual(None, html_attribute('src', None))
101
103
105 self.assertEqual(Markup('0<b>0</b> and <b>0</b>'),
106 Markup(tag(0, tag.b(0), ' and ', tag.b(0.0))))
107
109 self.assertEqual('<b>M</b>essäge',
110 unicode(tag(tag.b('M'), 'essäge')))
111
113 self.assertEqual(b'<b>M</b>ess\xc3\xa4ge',
114 str(tag(tag.b('M'), 'essäge')))
115
117 t = tag()
118 self.assertEqual(b'<b>M</b>',
119 str(t(tag.b('M'))))
120 t = tag()
121 self.assertEqual(b'<b>M</b>ess\xc3\xa4ge',
122 str(t(tag.b('M'), 'essäge')))
123
126
128 self.assertEqual(Markup('0<a>0</a> and <b>0</b> and <c/> and'
129 ' <d class="[\'a\', \'\', \'b\']"'
130 ' more_="[\'a\']"/>'),
131 Markup(xml(0, xml.a(0), ' and ', xml.b(0.0),
132 ' and ', xml.c(None), ' and ',
133 xml.d('', class_=[b'a', b'', b'b'],
134 more__=[b'a']))))
135
138
140 self.assertEqual(Markup('0<a>0</a> and <b>0</b> and <c></c>'
141 ' and <d class="a b" more_="[\'a\']"></d>'),
142 Markup(tag(0, tag.a(0, href=''), b' and ', tag.b(0.0),
143 ' and ', tag.c(None), ' and ',
144 tag.d('', class_=['a', '', 'b'],
145 more__=[b'a']))))
146
148 self.assertEqual('<b>M<em>essäge</em></b>',
149 unicode(tag.b('M', tag.em('essäge'))))
150
152 self.assertEqual(b'<b>M<em>ess\xc3\xa4ge</em></b>',
153 str(tag.b('M', tag.em('essäge'))))
154
180
183
184 safe_schemes = ('http', 'data')
185 safe_origins = ('data:', 'http://example.net', 'https://example.org/')
186
191
195
197 html = '<option value="1236" selected>Family B</option>'
198 self.assertEqual(
199 '<option selected="selected" value="1236">Family B</option>',
200 self.sanitize(html))
201
203 html = '<div style="top:expression(alert())">XSS</div>'
204 self.assertEqual('<div>XSS</div>', self.sanitize(html))
205
207 html = '<div style="top:EXPRESSION(alert())">XSS</div>'
208 self.assertEqual('<div>XSS</div>', self.sanitize(html))
209
221
223 html = (
224 '<div style="background-image:url(javascript:alert())">XSS</div>'
225 )
226 self.assertEqual('<div>XSS</div>', self.sanitize(html))
227
229 html = (
230 '<div style="background-image:URL(javascript:alert())">XSS</div>'
231 )
232 self.assertEqual('<div>XSS</div>', self.sanitize(html))
233
235 html = r'<div style="top:exp\72 ess\000069 on(alert())">XSS</div>'
236 self.assertEqual('<div>XSS</div>', self.sanitize(html))
237
238 html = r'<div style="top:exp\5c ression(alert())">XSS</div>'
239 self.assertEqual(r'<div style="top:exp\\ression(alert())">XSS</div>',
240 self.sanitize(html))
241 html = r'<div style="top:exp\5c 72 ession(alert())">XSS</div>'
242 self.assertEqual(r'<div style="top:exp\\72 ession(alert())">XSS</div>',
243 self.sanitize(html))
244
245 html = r'<div style="top:exp\000000res\1f sion(alert())">XSS</div>'
246 self.assertEqual('<div style="top:exp res sion(alert())">XSS</div>',
247 self.sanitize(html))
248
250 html = r'<div style="top:e\xp\ression(alert())">XSS</div>'
251 self.assertEqual('<div>XSS</div>', self.sanitize(html))
252 html = r'<div style="top:e\\xp\\ression(alert())">XSS</div>'
253 self.assertEqual(r'<div style="top:e\\xp\\ression(alert())">XSS</div>',
254 self.sanitize(html))
255
257 html = '<div style="POSITION:RELATIVE">XSS</div>'
258 self.assertEqual('<div>XSS</div>', self.sanitize(html))
259 html = '<div style="position:STATIC">safe</div>'
260 self.assertEqual('<div style="position:STATIC">safe</div>',
261 self.sanitize(html))
262 html = '<div style="behavior:url(test.htc)">XSS</div>'
263 self.assertEqual('<div>XSS</div>', self.sanitize(html))
264 html = '<div style="-ms-behavior:url(test.htc) url(#obj)">XSS</div>'
265 self.assertEqual('<div>XSS</div>', self.sanitize(html))
266 html = ("""<div style="-o-link:'javascript:alert(1)';"""
267 """-o-link-source:current">XSS</div>""")
268 self.assertEqual('<div>XSS</div>', self.sanitize(html))
269 html = """<div style="-moz-binding:url(xss.xbl)">XSS</div>"""
270 self.assertEqual('<div>XSS</div>', self.sanitize(html))
271
273 html = '<div style="margin-top:-9999px">XSS</div>'
274 self.assertEqual('<div>XSS</div>', self.sanitize(html))
275 html = '<div style="margin:0 -9999px">XSS</div>'
276 self.assertEqual('<div>XSS</div>', self.sanitize(html))
277
279 html = '<div style="*position:static">XSS</div>'
280 self.assertEqual('<div>XSS</div>', self.sanitize(html))
281 html = '<div style="_margin:-10px">XSS</div>'
282 self.assertEqual('<div>XSS</div>', self.sanitize(html))
283
285 html = ('<div style="display:none;border-left-color:red;'
286 'user_defined:1;-moz-user-selct:-moz-all">prop</div>')
287 self.assertEqual('<div style="display:none; border-left-color:red'
288 '">prop</div>',
289 self.sanitize(html))
290
292
293 html = '<div style="top:expression(alert())">XSS</div>'
294 self.assertEqual('<div>XSS</div>', self.sanitize(html))
295
296 html = '<div style="top:EXPRESSION(alert())">XSS</div>'
297 self.assertEqual('<div>XSS</div>', self.sanitize(html))
298
299 html = '<div style="top:expʀessɪoɴ(alert())">XSS</div>'
300 self.assertEqual('<div>XSS</div>', self.sanitize(html))
301
303
304 html = (
305 '<div style="background-image:uʀʟ(javascript:alert())">XSS</div>'
306 )
307 self.assertEqual('<div>XSS</div>', self.sanitize(html))
308
310 test = self._assert_sanitize
311
312 test('<img src="data:image/png,...."/>',
313 '<img src="data:image/png,...."/>')
314 test('<img src="http://example.org/login" crossorigin="anonymous"/>',
315 '<img src="http://example.org/login"/>')
316 test('<img src="http://example.org/login" crossorigin="anonymous"/>',
317 '<img src="http://example.org/login"'
318 ' crossorigin="use-credentials"/>')
319 test('<img src="http://example.net/bar.png"/>',
320 '<img src="http://example.net/bar.png"/>')
321 test('<img src="http://example.net:443/qux.png"'
322 ' crossorigin="anonymous"/>',
323 '<img src="http://example.net:443/qux.png"/>')
324 test('<img src="/path/foo.png"/>', '<img src="/path/foo.png"/>')
325 test('<img src="../../bar.png"/>', '<img src="../../bar.png"/>')
326 test('<img src="qux.png"/>', '<img src="qux.png"/>')
327
328 test('<div>x</div>',
329 '<div style="background:url(http://example.org/login)">x</div>')
330 test('<div style="background:url(http://example.net/1.png)">x</div>',
331 '<div style="background:url(http://example.net/1.png)">x</div>')
332 test('<div>x</div>',
333 '<div style="background:url(http://example.net:443/1.png)">'
334 'x</div>')
335 test('<div style="background:url(data:image/png,...)">x</div>',
336 '<div style="background:url(data:image/png,...)">x</div>')
337 test('<div>x</div>',
338 '<div style="background:url(//example.net/foo.png)">x</div>')
339 test('<div style="background:url(/path/to/foo.png)">safe</div>',
340 '<div style="background:url(/path/to/foo.png)">safe</div>')
341 test('<div style="background:url(../../bar.png)">safe</div>',
342 '<div style="background:url(../../bar.png)">safe</div>')
343 test('<div style="background:url(qux.png)">safe</div>',
344 '<div style="background:url(qux.png)">safe</div>')
345
347 test = self._assert_sanitize
348 test('<p>&hellip;</p>', '<p>&hellip;</p>')
349 test('<p>&</p>', '<p>&</p>')
350 test('<p>&</p>', '<p>&</p>')
351 test('<p>&<></p>', '<p>&<></p>')
352 test('<p>&&</p>', '<p>&&</p>')
353 test('<p>&\u2026</p>', '<p>&…</p>')
354 test("<p>&unknown;</p>", '<p>&unknown;</p>')
355 test("<p>\U0010ffff</p>", '<p></p>')
356 test("<p>\U0010ffff</p>", '<p></p>')
357 test("<p>\U0010ffff</p>", '<p></p>')
358 test("<p>&#1114112;</p>", '<p>�</p>')
359 test("<p>&#x110000;</p>", '<p>�</p>')
360 test("<p>&#X110000;</p>", '<p>�</p>')
361 test("<p>&#abcd;</p>", '<p>&#abcd;</p>')
362 test('<p>&#%d;</p>' % (sys.maxint + 1),
363 '<p>&#%d;</p>' % (sys.maxint + 1))
364
366 self._assert_sanitize('<img title="&"/>', '<img title="&"/>')
367 self._assert_sanitize('''<img title="&<>"'"/>''',
368 '''<img title="&<>"'"/>''')
369 self._assert_sanitize('''<img title="&<>"'"/>''',
370 '''<img title="&<>"'"/>''')
371 self._assert_sanitize("""<img title="&<>'"/>""",
372 """<img title="&<>'"/>""")
373 self._assert_sanitize('<img title="&&"/>',
374 '<img title="&&"/>')
375 self._assert_sanitize('<img title="&\u2026"/>',
376 '<img title="&…"/>')
377 self._assert_sanitize('<img title="&hellip;"/>',
378 '<img title="&hellip;"/>')
379 self._assert_sanitize('<img title="&unknown;"/>',
380 '<img title="&unknown;"/>')
381 self._assert_sanitize('<img title="\U0010ffff"/>',
382 '<img title=""/>')
383 self._assert_sanitize('<img title="\U0010ffff"/>',
384 '<img title=""/>')
385 self._assert_sanitize('<img title="\U0010ffff"/>',
386 '<img title=""/>')
387 self._assert_sanitize('<img title="&#1114112;"/>',
388 '<img title="�"/>')
389 self._assert_sanitize('<img title="&#x110000;"/>',
390 '<img title="�"/>')
391 self._assert_sanitize('<img title="&#X110000;"/>',
392 '<img title="�"/>')
393 self._assert_sanitize('<img title="&#abcd;"/>',
394 '<img title="&#abcd;"/>')
395 self._assert_sanitize('<img title="&#%d;"/>' % (sys.maxint + 1),
396 '<img title="&#%d;"/>' % (sys.maxint + 1))
397
399 self.assertEqual(expected, self.sanitize(content))
400
403
405 test = self._assert_sanitize
406 test("<p>&<>"'</p>",
407 '<p>&<>"'</p>')
408 test("<p>&<>"'</p>",
409 '<p>&<>"'</p>')
410
411
412 if genshi:
418
420 test = self._assert_sanitize
421 test('''<p>&<>"'</p>''',
422 '<p>&<>"'</p>')
423 test('''<p>&<>"'</p>''',
424 '<p>&<>"'</p>')
425
428
430 frag = tag(tag.p('Paragraph with a ',
431 tag.a('link', href='http://www.edgewall.org'),
432 ' and some ', tag.strong('strong text')))
433 self.assertIsNotNone(find_element(frag, tag='p'))
434 result = find_element(frag, tag='a')
435 self.assertIsNotNone(result)
436 self.assertEqual('<a href="http://www.edgewall.org">link</a>',
437 str(result))
438 result = find_element(frag, tag='strong')
439 self.assertIsNotNone(result)
440 self.assertEqual('<strong>strong text</strong>', str(result))
441 self.assertIsNone(find_element(frag, tag='input'))
442 self.assertIsNone(find_element(frag, tag='textarea'))
443
444 @unittest.skipUnless(LazyProxy, 'Babel unavailable')
446 lazyproxy = tgettext('Text with a %(a)s and some %(b)s',
447 a=tag.a('link', href='http://www.edgewall.org'),
448 b=tag.strong('strong text'))
449 self.assertIsNotNone(find_element(lazyproxy, tag='a'))
450 self.assertEqual('<a href="http://www.edgewall.org">link</a>',
451 str(find_element(lazyproxy, tag='a')))
452 result = find_element(lazyproxy, tag='strong')
453 self.assertIsNotNone(result)
454 self.assertEqual('<strong>strong text</strong>', str(result))
455 self.assertIsNone(find_element(lazyproxy, tag='input'))
456 self.assertIsNone(find_element(lazyproxy, tag='textarea'))
457
460
469
478
480 uris = ['https://example.org/', 'http://example.net']
481 self.assertFalse(is_safe_origin(uris, 'data:text/plain,blah'))
482 self.assertTrue(is_safe_origin(uris, 'https://example.org'))
483 self.assertTrue(is_safe_origin(uris, 'https://example.org/'))
484 self.assertTrue(is_safe_origin(uris, 'https://example.org/path/'))
485 self.assertTrue(is_safe_origin(uris, 'http://example.net'))
486 self.assertTrue(is_safe_origin(uris, 'http://example.net/'))
487 self.assertTrue(is_safe_origin(uris, 'http://example.net/path'))
488 self.assertFalse(is_safe_origin(uris, 'https://example.com'))
489 self.assertFalse(is_safe_origin(uris, 'blob:'))
490 self.assertTrue(is_safe_origin(uris, '/path/to'))
491 self.assertTrue(is_safe_origin(uris, 'file.txt'))
492
494 uris = ['https://example.org/path/to', 'http://example.net/path/to/']
495 self.assertFalse(is_safe_origin(uris, 'https://example.org'))
496 self.assertFalse(is_safe_origin(uris, 'https://example.org/'))
497 self.assertFalse(is_safe_origin(uris, 'https://example.org/path'))
498 self.assertFalse(is_safe_origin(uris, 'https://example.org/path/'))
499 self.assertTrue(is_safe_origin(uris, 'https://example.org/path/to'))
500 self.assertTrue(is_safe_origin(uris, 'https://example.org/path/to/'))
501 self.assertTrue(is_safe_origin(
502 uris, 'https://example.org/path/to/image.png'))
503 self.assertFalse(is_safe_origin(uris, 'http://example.net'))
504 self.assertFalse(is_safe_origin(uris, 'http://example.net/'))
505 self.assertFalse(is_safe_origin(uris, 'http://example.net/path'))
506 self.assertFalse(is_safe_origin(uris, 'http://example.net/path/'))
507 self.assertFalse(is_safe_origin(uris, 'http://example.net/path/to'))
508 self.assertTrue(is_safe_origin(uris, 'http://example.net/path/to/'))
509 self.assertTrue(is_safe_origin(
510 uris, 'http://example.net/path/to/image.png'))
511 self.assertFalse(is_safe_origin(uris, 'blob:'))
512 self.assertTrue(is_safe_origin(uris, '/path/to'))
513 self.assertTrue(is_safe_origin(uris, 'file.txt'))
514
515
516 -class PlaintextTestCase(unittest.TestCase):
517
519 self.assertEqual('Back to &<>"\'',
520 plaintext('Back to &<>"''))
521
523 fragment = tag('Back to ', tag.span('&<>"''))
524 self.assertEqual('Back to &<>"'',
525 plaintext(fragment))
526
527 @unittest.skipUnless(LazyProxy, 'Babel unavailable')
529 lazyproxy = gettext('Back to %(parent)s',
530 parent='&<>"'')
531 self.assertEqual('Back to &<>"\'', plaintext(lazyproxy))
532
533 @unittest.skipUnless(LazyProxy, 'Babel unavailable')
535 lazyproxy = tgettext('Back to %(parent)s',
536 parent=tag.span('&<>"''))
537 self.assertEqual('Back to &<>"'',
538 plaintext(lazyproxy))
539
542
544 rv = to_fragment('blah')
545 self.assertEqual(Fragment, type(rv))
546 self.assertEqual('blah', unicode(rv))
547
549 rv = to_fragment(tag('blah'))
550 self.assertEqual(Fragment, type(rv))
551 self.assertEqual('blah', unicode(rv))
552
554 rv = to_fragment(tag.p('blah'))
555 self.assertEqual(Element, type(rv))
556 self.assertEqual('<p>blah</p>', unicode(rv))
557
559 rv = to_fragment(TracError('blah'))
560 self.assertEqual(Fragment, type(rv))
561 self.assertEqual('blah', unicode(rv))
562
564 message = tag('Powered by ',
565 tag.a('Trac', href='https://trac.edgewall.org/'))
566 rv = to_fragment(TracError(message))
567 self.assertEqual(Fragment, type(rv))
568 self.assertEqual('Powered by <a href="https://trac.edgewall.org/">Trac'
569 '</a>', unicode(rv))
570
572 message = tag.p('Powered by ',
573 tag.a('Trac', href='https://trac.edgewall.org/'))
574 rv = to_fragment(TracError(message))
575 self.assertEqual(Element, type(rv))
576 self.assertEqual('<p>Powered by <a href="https://trac.edgewall.org/">'
577 'Trac</a></p>', unicode(rv))
578
586
588 message = tag.p('Powered by ',
589 tag.a('Trac', href='https://trac.edgewall.org/'))
590 rv = to_fragment(TracError(TracError(message)))
591 self.assertEqual(Element, type(rv))
592 self.assertEqual('<p>Powered by <a href="https://trac.edgewall.org/">'
593 'Trac</a></p>', unicode(rv))
594
596 rv = to_fragment(ValueError('invalid literal for int(): blah'))
597 self.assertEqual(Fragment, type(rv))
598 self.assertEqual('invalid literal for int(): blah', unicode(rv))
599
601 rv = to_fragment(ValueError(tag('invalid literal for int(): ',
602 tag.b('blah'))))
603 self.assertEqual(Fragment, type(rv))
604 self.assertEqual('invalid literal for int(): <b>blah</b>', unicode(rv))
605
607 v1 = ValueError(tag('invalid literal for int(): ', tag.b('blah')))
608 rv = to_fragment(ValueError(v1))
609 self.assertEqual(Fragment, type(rv))
610 self.assertEqual('invalid literal for int(): <b>blah</b>', unicode(rv))
611
612 - def test_gettext(self):
613 rv = to_fragment(gettext('%(size)s bytes', size=0))
614 self.assertEqual(Fragment, type(rv))
615 self.assertEqual('0 bytes', unicode(rv))
616
617 - def test_tgettext(self):
618 rv = to_fragment(tgettext('Back to %(parent)s',
619 parent=tag.a('WikiStart',
620 href='http://localhost/')))
621 self.assertEqual(Fragment, type(rv))
622 self.assertEqual('Back to <a href="http://localhost/">WikiStart</a>',
623 unicode(rv))
624
626 e = TracError(gettext('%(size)s bytes', size=0))
627 rv = to_fragment(e)
628 self.assertEqual(Fragment, type(rv))
629 self.assertEqual('0 bytes', unicode(rv))
630
632 e = TracError(tgettext('Back to %(parent)s',
633 parent=tag.a('WikiStart',
634 href='http://localhost/')))
635 rv = to_fragment(e)
636 self.assertEqual(Fragment, type(rv))
637 self.assertEqual('Back to <a href="http://localhost/">WikiStart</a>',
638 unicode(rv))
639
641 try:
642 open(filename)
643 except IOError as e:
644 return e
645 else:
646 self.fail('IOError not raised')
647
649 rv = to_fragment(self._ioerror(b'./notfound'))
650 self.assertEqual(Fragment, type(rv))
651 self.assertEqual("[Errno 2] No such file or directory: './notfound'",
652 unicode(rv))
653
655 e = self._ioerror(b'./notfound')
656 rv = to_fragment(ValueError(e))
657 self.assertEqual(Fragment, type(rv))
658 self.assertEqual("[Errno 2] No such file or directory: './notfound'",
659 unicode(rv))
660
679
680
681 if __name__ == '__main__':
682 unittest.main(defaultTest='test_suite')
683