| 1 | # -*- coding: utf-8 -*-
|
|---|
| 2 |
|
|---|
| 3 | import unittest
|
|---|
| 4 | from genshi.builder import Element, Fragment, tag
|
|---|
| 5 | from genshi.input import HTML
|
|---|
| 6 |
|
|---|
| 7 | from trac.core import TracError
|
|---|
| 8 | from trac.util.html import TracHTMLSanitizer, to_fragment
|
|---|
| 9 | from trac.util.translation import gettext, tgettext
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 | class TracHTMLSanitizerTestCase(unittest.TestCase):
|
|---|
| 13 | def test_expression(self):
|
|---|
| 14 | html = HTML(r'<div style="top:expression(alert())">XSS</div>')
|
|---|
| 15 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 16 |
|
|---|
| 17 | def test_capital_expression(self):
|
|---|
| 18 | html = HTML(r'<div style="top:EXPRESSION(alert())">XSS</div>')
|
|---|
| 19 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 20 |
|
|---|
| 21 | def test_expression_with_comments(self):
|
|---|
| 22 | html = HTML(r'<div style="top:exp/**/ression(alert())">XSS</div>')
|
|---|
| 23 | self.assertEqual('<div style="top:exp ression(alert())">XSS</div>',
|
|---|
| 24 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 25 | html = HTML(r'<div style="top:exp//**/**/ression(alert())">XSS</div>')
|
|---|
| 26 | self.assertEqual(
|
|---|
| 27 | '<div style="top:exp/ **/ression(alert())">XSS</div>',
|
|---|
| 28 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 29 | html = HTML(r'<div style="top:ex/*p*/ression(alert())">XSS</div>')
|
|---|
| 30 | self.assertEqual('<div style="top:ex ression(alert())">XSS</div>',
|
|---|
| 31 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 32 |
|
|---|
| 33 | def test_url_with_javascript(self):
|
|---|
| 34 | html = HTML('<div style="background-image:url(javascript:alert())">'
|
|---|
| 35 | 'XSS</div>')
|
|---|
| 36 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 37 |
|
|---|
| 38 | def test_capital_url_with_javascript(self):
|
|---|
| 39 | html = HTML('<div style="background-image:URL(javascript:alert())">'
|
|---|
| 40 | 'XSS</div>')
|
|---|
| 41 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 42 |
|
|---|
| 43 | def test_unicode_escapes(self):
|
|---|
| 44 | html = HTML(r'<div style="top:exp\72 ess\000069 on(alert())">'
|
|---|
| 45 | r'XSS</div>')
|
|---|
| 46 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 47 | # escaped backslash
|
|---|
| 48 | html = HTML(r'<div style="top:exp\5c ression(alert())">XSS</div>')
|
|---|
| 49 | self.assertEqual(r'<div style="top:exp\\ression(alert())">XSS</div>',
|
|---|
| 50 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 51 | html = HTML(r'<div style="top:exp\5c 72 ession(alert())">XSS</div>')
|
|---|
| 52 | self.assertEqual(r'<div style="top:exp\\72 ession(alert())">XSS</div>',
|
|---|
| 53 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 54 | # escaped control characters
|
|---|
| 55 | html = HTML(r'<div style="top:exp\000000res\1f sion(alert())">'
|
|---|
| 56 | r'XSS</div>')
|
|---|
| 57 | self.assertEqual('<div style="top:exp res sion(alert())">XSS</div>',
|
|---|
| 58 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 59 |
|
|---|
| 60 | def test_backslash_without_hex(self):
|
|---|
| 61 | html = HTML(r'<div style="top:e\xp\ression(alert())">XSS</div>')
|
|---|
| 62 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 63 | html = HTML(r'<div style="top:e\\xp\\ression(alert())">XSS</div>')
|
|---|
| 64 | self.assertEqual(r'<div style="top:e\\xp\\ression(alert())">'
|
|---|
| 65 | 'XSS</div>',
|
|---|
| 66 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 67 |
|
|---|
| 68 | def test_unsafe_props(self):
|
|---|
| 69 | html = HTML('<div style="POSITION:RELATIVE">XSS</div>')
|
|---|
| 70 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 71 | html = HTML('<div style="position:STATIC">safe</div>')
|
|---|
| 72 | self.assertEqual('<div style="position:STATIC">safe</div>',
|
|---|
| 73 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 74 |
|
|---|
| 75 | html = HTML('<div style="behavior:url(test.htc)">XSS</div>')
|
|---|
| 76 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 77 |
|
|---|
| 78 | html = HTML('<div style="-ms-behavior:url(test.htc) url(#obj)">'
|
|---|
| 79 | 'XSS</div>')
|
|---|
| 80 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 81 |
|
|---|
| 82 | html = HTML("""<div style="-o-link:'javascript:alert(1)';"""
|
|---|
| 83 | """-o-link-source:current">XSS</div>""")
|
|---|
| 84 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 85 |
|
|---|
| 86 | html = HTML("""<div style="-moz-binding:url(xss.xbl)">XSS</div>""")
|
|---|
| 87 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 88 |
|
|---|
| 89 | def test_nagative_margin(self):
|
|---|
| 90 | html = HTML('<div style="margin-top:-9999px">XSS</div>')
|
|---|
| 91 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 92 | html = HTML('<div style="margin:0 -9999px">XSS</div>')
|
|---|
| 93 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 94 |
|
|---|
| 95 | def test_css_hack(self):
|
|---|
| 96 | html = HTML('<div style="*position:static">XSS</div>')
|
|---|
| 97 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 98 |
|
|---|
| 99 | html = HTML('<div style="_margin:-10px">XSS</div>')
|
|---|
| 100 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 101 |
|
|---|
| 102 | def test_property_name(self):
|
|---|
| 103 | html = HTML('<div style="display:none;border-left-color:red;'
|
|---|
| 104 | 'user_defined:1;-moz-user-selct:-moz-all">prop</div>')
|
|---|
| 105 | self.assertEqual('<div style="display:none; border-left-color:red'
|
|---|
| 106 | '">prop</div>',
|
|---|
| 107 | unicode(html | TracHTMLSanitizer()))
|
|---|
| 108 |
|
|---|
| 109 | def test_unicode_expression(self):
|
|---|
| 110 | # Fullwidth small letters
|
|---|
| 111 | html = HTML(u'<div style="top:expression(alert())">'
|
|---|
| 112 | u'XSS</div>')
|
|---|
| 113 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 114 | # Fullwidth capital letters
|
|---|
| 115 | html = HTML(u'<div style="top:EXPRESSION(alert())">'
|
|---|
| 116 | u'XSS</div>')
|
|---|
| 117 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 118 | # IPA extensions
|
|---|
| 119 | html = HTML(u'<div style="top:expʀessɪoɴ(alert())">'
|
|---|
| 120 | u'XSS</div>')
|
|---|
| 121 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 122 |
|
|---|
| 123 | def test_unicode_url(self):
|
|---|
| 124 | # IPA extensions
|
|---|
| 125 | html = HTML(u'<div style="background-image:uʀʟ(javascript:alert())">'
|
|---|
| 126 | u'XSS</div>')
|
|---|
| 127 | self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
|
|---|
| 128 |
|
|---|
| 129 |
|
|---|
| 130 | class ToFragmentTestCase(unittest.TestCase):
|
|---|
| 131 |
|
|---|
| 132 | def test_unicode(self):
|
|---|
| 133 | rv = to_fragment('blah')
|
|---|
| 134 | self.assertEqual(Fragment, type(rv))
|
|---|
| 135 | self.assertEqual('blah', unicode(rv))
|
|---|
| 136 |
|
|---|
| 137 | def test_fragment(self):
|
|---|
| 138 | rv = to_fragment(tag('blah'))
|
|---|
| 139 | self.assertEqual(Fragment, type(rv))
|
|---|
| 140 | self.assertEqual('blah', unicode(rv))
|
|---|
| 141 |
|
|---|
| 142 | def test_element(self):
|
|---|
| 143 | rv = to_fragment(tag.p('blah'))
|
|---|
| 144 | self.assertEqual(Element, type(rv))
|
|---|
| 145 | self.assertEqual('<p>blah</p>', unicode(rv))
|
|---|
| 146 |
|
|---|
| 147 | def test_tracerror(self):
|
|---|
| 148 | rv = to_fragment(TracError('blah'))
|
|---|
| 149 | self.assertEqual(Fragment, type(rv))
|
|---|
| 150 | self.assertEqual('blah', unicode(rv))
|
|---|
| 151 |
|
|---|
| 152 | def test_tracerror_with_fragment(self):
|
|---|
| 153 | message = tag('Powered by ',
|
|---|
| 154 | tag.a('Trac', href='http://trac.edgewall.org/'))
|
|---|
| 155 | rv = to_fragment(TracError(message))
|
|---|
| 156 | self.assertEqual(Fragment, type(rv))
|
|---|
| 157 | self.assertEqual('Powered by <a href="http://trac.edgewall.org/">Trac'
|
|---|
| 158 | '</a>', unicode(rv))
|
|---|
| 159 |
|
|---|
| 160 | def test_tracerror_with_element(self):
|
|---|
| 161 | message = tag.p('Powered by ',
|
|---|
| 162 | tag.a('Trac', href='http://trac.edgewall.org/'))
|
|---|
| 163 | rv = to_fragment(TracError(message))
|
|---|
| 164 | self.assertEqual(Element, type(rv))
|
|---|
| 165 | self.assertEqual('<p>Powered by <a href="http://trac.edgewall.org/">'
|
|---|
| 166 | 'Trac</a></p>', unicode(rv))
|
|---|
| 167 |
|
|---|
| 168 | def test_error(self):
|
|---|
| 169 | rv = to_fragment(ValueError('invalid literal for int(): blah'))
|
|---|
| 170 | self.assertEqual(Fragment, type(rv))
|
|---|
| 171 | self.assertEqual('invalid literal for int(): blah', unicode(rv))
|
|---|
| 172 |
|
|---|
| 173 | def test_gettext(self):
|
|---|
| 174 | rv = to_fragment(gettext('%(size)s bytes', size=0))
|
|---|
| 175 | self.assertEqual(Fragment, type(rv))
|
|---|
| 176 | self.assertEqual('0 bytes', unicode(rv))
|
|---|
| 177 |
|
|---|
| 178 | def test_tgettext(self):
|
|---|
| 179 | rv = to_fragment(tgettext('Back to %(parent)s',
|
|---|
| 180 | parent=tag.a('WikiStart',
|
|---|
| 181 | href='http://localhost/')))
|
|---|
| 182 | self.assertEqual(Fragment, type(rv))
|
|---|
| 183 | self.assertEqual('Back to <a href="http://localhost/">WikiStart</a>',
|
|---|
| 184 | unicode(rv))
|
|---|
| 185 |
|
|---|
| 186 | def test_tracerror_with_gettext(self):
|
|---|
| 187 | e = TracError(gettext('%(size)s bytes', size=0))
|
|---|
| 188 | rv = to_fragment(e)
|
|---|
| 189 | self.assertEqual(Fragment, type(rv))
|
|---|
| 190 | self.assertEqual('0 bytes', unicode(rv))
|
|---|
| 191 |
|
|---|
| 192 | def test_tracerror_with_tgettext(self):
|
|---|
| 193 | e = TracError(tgettext('Back to %(parent)s',
|
|---|
| 194 | parent=tag.a('WikiStart',
|
|---|
| 195 | href='http://localhost/')))
|
|---|
| 196 | rv = to_fragment(e)
|
|---|
| 197 | self.assertEqual(Fragment, type(rv))
|
|---|
| 198 | self.assertEqual('Back to <a href="http://localhost/">WikiStart</a>',
|
|---|
| 199 | unicode(rv))
|
|---|
| 200 |
|
|---|
| 201 |
|
|---|
| 202 | def suite():
|
|---|
| 203 | suite = unittest.TestSuite()
|
|---|
| 204 | suite.addTest(unittest.makeSuite(TracHTMLSanitizerTestCase, 'test'))
|
|---|
| 205 | suite.addTest(unittest.makeSuite(ToFragmentTestCase, 'test'))
|
|---|
| 206 | return suite
|
|---|
| 207 |
|
|---|
| 208 | if __name__ == '__main__':
|
|---|
| 209 | unittest.main(defaultTest='suite')
|
|---|