[IMP] html_sanitize: test with assertIn
[odoo/odoo.git] / openerp / tests / test_mail.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # This test can be run stand-alone with something like:
4 # > PYTHONPATH=. python2 openerp/tests/test_misc.py
5 ##############################################################################
6 #
7 #    OpenERP, Open Source Business Applications
8 #    Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
9 #
10 #    This program is free software: you can redistribute it and/or modify
11 #    it under the terms of the GNU Affero General Public License as
12 #    published by the Free Software Foundation, either version 3 of the
13 #    License, or (at your option) any later version.
14 #
15 #    This program is distributed in the hope that it will be useful,
16 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #    GNU Affero General Public License for more details.
19 #
20 #    You should have received a copy of the GNU Affero General Public License
21 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 #
23 ##############################################################################
24
25 import unittest2
26 from openerp.tools import html_sanitize, html_email_clean, append_content_to_html, plaintext2html
27
28 HTML_SOURCE = """
29 <font size="2" style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; ">test1</font>
30 <div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; font-size: 12px; font-style: normal; ">
31 <b>test2</b></div><div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; font-size: 12px; ">
32 <i>test3</i></div><div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; font-size: 12px; ">
33 <u>test4</u></div><div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; font-size: 12px; ">
34 <strike>test5</strike></div><div style="color: rgb(31, 31, 31); font-family: monospace; font-variant: normal; line-height: normal; ">
35 <font size="5">test6</font></div><div><ul><li><font color="#1f1f1f" face="monospace" size="2">test7</font></li><li>
36 <font color="#1f1f1f" face="monospace" size="2">test8</font></li></ul><div><ol><li><font color="#1f1f1f" face="monospace" size="2">test9</font>
37 </li><li><font color="#1f1f1f" face="monospace" size="2">test10</font></li></ol></div></div>
38 <blockquote style="margin: 0 0 0 40px; border: none; padding: 0px;"><div><div><div><font color="#1f1f1f" face="monospace" size="2">
39 test11</font></div></div></div></blockquote><blockquote style="margin: 0 0 0 40px; border: none; padding: 0px;">
40 <blockquote style="margin: 0 0 0 40px; border: none; padding: 0px;"><div><font color="#1f1f1f" face="monospace" size="2">
41 test12</font></div><div><font color="#1f1f1f" face="monospace" size="2"><br></font></div></blockquote></blockquote>
42 <font color="#1f1f1f" face="monospace" size="2"><a href="http://google.com">google</a></font>
43 <a href="javascript:alert('malicious code')">test link</a>
44 """
45
46 EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
47     <p>Hello ${object.partner_id.name},</p>
48     <p>A new invoice is available for you: </p>
49     <p style="border-left: 1px solid #8e0000; margin-left: 30px;">
50        &nbsp;&nbsp;<strong>REFERENCES</strong><br />
51        &nbsp;&nbsp;Invoice number: <strong>${object.number}</strong><br />
52        &nbsp;&nbsp;Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
53        &nbsp;&nbsp;Invoice date: ${object.date_invoice}<br />
54        &nbsp;&nbsp;Order reference: ${object.origin}<br />
55        &nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
56     </p>
57     <br/>
58     <p>It is also possible to directly pay with Paypal:</p>
59     <a style="margin-left: 120px;" href="${object.paypal_url}">
60         <img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
61     </a>
62     <br/>
63     <p>If you have any question, do not hesitate to contact us.</p>
64     <p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
65     <br/>
66     <br/>
67     <div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
68         <h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
69             <strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
70     </div>
71     <div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
72         <span style="color: #222; margin-bottom: 5px; display: block; ">
73         ${object.company_id.street}<br/>
74         ${object.company_id.street2}<br/>
75         ${object.company_id.zip} ${object.company_id.city}<br/>
76         ${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
77         </span>
78         <div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
79             Phone:&nbsp; ${object.company_id.phone}
80         </div>
81         <div>
82             Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
83         </div>
84     </div>
85 </div></body></html>"""
86
87 TEXT_MAIL1 = """I contact you about our meeting for tomorrow. Here is the schedule I propose:
88 9 AM: brainstorming about our new amazing business app</span></li>
89 9.45 AM: summary
90 10 AM: meeting with Fabien to present our app
91 Is everything ok for you ?
92 --
93 Administrator"""
94
95 HTML_MAIL1 = """<div>
96 <font><span>I contact you about our meeting for tomorrow. Here is the schedule I propose:</span></font>
97 </div>
98 <div><ul>
99 <li><span>9 AM: brainstorming about our new amazing business app</span></li>
100 <li><span>9.45 AM: summary</span></li>
101 <li><span>10 AM: meeting with Fabien to present our app</span></li>
102 </ul></div>
103 <div><font><span>Is everything ok for you ?</span></font></div>"""
104
105 GMAIL_REPLY1_SAN = """Hello,<div><br></div><div>Ok for me. I am replying directly in gmail, without signature.</div><div><br></div><div>Kind regards,</div><div><br></div><div>Demo.<br><br><div>On Thu, Nov 8, 2012 at 5:29 PM,  <span>&lt;<a href="mailto:dummy@example.com">dummy@example.com</a>&gt;</span> wrote:<br><blockquote><div>I contact you about our meeting for tomorrow. Here is the schedule I propose:</div><div><ul><li>9 AM: brainstorming about our new amazing business app&lt;/span&gt;&lt;/li&gt;</li>
106 <li>9.45 AM: summary</li><li>10 AM: meeting with Fabien to present our app</li></ul></div><div>Is everything ok for you ?</div>
107 <div><p>--<br>Administrator</p></div>
108
109 <div><p>Log in our portal at: <a href="http://localhost:8069#action=login&amp;db=mail_1&amp;login=demo">http://localhost:8069#action=login&amp;db=mail_1&amp;login=demo</a></p></div>
110 </blockquote></div><br></div>"""
111
112 THUNDERBIRD_16_REPLY1_SAN = """    <div>On 11/08/2012 05:29 PM,
113       <a href="mailto:dummy@example.com">dummy@example.com</a> wrote:<br></div>
114     <blockquote>
115       <div>I contact you about our meeting for tomorrow. Here is the
116         schedule I propose:</div>
117       <div>
118         <ul><li>9 AM: brainstorming about our new amazing business
119             app&lt;/span&gt;&lt;/li&gt;</li>
120           <li>9.45 AM: summary</li>
121           <li>10 AM: meeting with Fabien to present our app</li>
122         </ul></div>
123       <div>Is everything ok for you ?</div>
124       <div>
125         <p>--<br>
126           Administrator</p>
127       </div>
128       <div>
129         <p>Log in our portal at:
130 <a href="http://localhost:8069#action=login&amp;db=mail_1&amp;token=rHdWcUART5PhEnJRaXjH">http://localhost:8069#action=login&amp;db=mail_1&amp;token=rHdWcUART5PhEnJRaXjH</a></p>
131       </div>
132     </blockquote>
133     Ok for me. I am replying directly below your mail, using
134     Thunderbird, with a signature.<br><br>
135     Did you receive my email about my new laptop, by the way ?<br><br>
136     Raoul.<br><pre>-- 
137 Raoul Grosbedonn&#233;e
138 </pre>"""
139
140 TEXT_TPL = """Salut Raoul!
141 Le 28 oct. 2012 à 00:02, Raoul Grosbedon a écrit :
142
143 > C'est sûr que je suis intéressé (quote)!
144
145 Trouloulou pouet pouet. Je ne vais quand même pas écrire de vrais mails, non mais ho.
146
147 > 2012/10/27 Bert Tartopoils :
148 >> Diantre, me disè-je en envoyant un message similaire à Martine, mais comment vas-tu (quote)?
149 >> 
150 >> A la base le contenu était un vrai mail, mais je l'ai quand même réécrit pour ce test, histoire de dire que, quand même, on ne met pas n'importe quoi ici. (quote)
151 >> 
152 >> Et sinon bon courage pour trouver tes clefs (quote).
153 >> 
154 >> Bert TARTOPOILS
155 >> bert.tartopoils@miam.miam
156 >> 
157
158
159 > -- 
160 > Raoul Grosbedon
161
162 Bert TARTOPOILS
163 bert.tartopoils@miam.miam
164 """
165
166
167 class TestSanitizer(unittest2.TestCase):
168     """ Test the html sanitizer that filters html to remove unwanted attributes """
169
170     def test_basic_sanitizer(self):
171         cases = [
172             ("yop", "<p>yop</p>"),  # simple
173             ("lala<p>yop</p>xxx", "<div><p>lala</p><p>yop</p>xxx</div>"),  # trailing text
174             ("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci",
175                 u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"),  # unicode
176         ]
177         for content, expected in cases:
178             html = html_sanitize(content)
179             self.assertEqual(html, expected, 'html_sanitize is broken')
180
181     def test_evil_malicious_code(self):
182         # taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Tests
183         cases = [
184             ("<IMG SRC=javascript:alert('XSS')>"),  # no quotes and semicolons
185             ("<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>"),  # UTF-8 Unicode encoding
186             ("<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>"),  # hex encoding
187             ("<IMG SRC=\"jav&#x0D;ascript:alert('XSS');\">"),  # embedded carriage return
188             ("<IMG SRC=\"jav&#x0A;ascript:alert('XSS');\">"),  # embedded newline
189             ("<IMG SRC=\"jav   ascript:alert('XSS');\">"),  # embedded tab
190             ("<IMG SRC=\"jav&#x09;ascript:alert('XSS');\">"),  # embedded encoded tab
191             ("<IMG SRC=\" &#14;  javascript:alert('XSS');\">"),  # spaces and meta-characters
192             ("<IMG SRC=\"javascript:alert('XSS')\""),  # half-open html
193             ("<IMG \"\"\"><SCRIPT>alert(\"XSS\")</SCRIPT>\">"),  # malformed tag
194             ("<SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"),  # non-alpha-non-digits
195             ("<SCRIPT/SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"),  # non-alpha-non-digits
196             ("<<SCRIPT>alert(\"XSS\");//<</SCRIPT>"),  # extraneous open brackets
197             ("<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >"),  # non-closing script tags
198             ("<INPUT TYPE=\"IMAGE\" SRC=\"javascript:alert('XSS');\">"),  # input image
199             ("<BODY BACKGROUND=\"javascript:alert('XSS')\">"),  # body image
200             ("<IMG DYNSRC=\"javascript:alert('XSS')\">"),  # img dynsrc
201             ("<IMG LOWSRC=\"javascript:alert('XSS')\">"),  # img lowsrc
202             ("<TABLE BACKGROUND=\"javascript:alert('XSS')\">"),  # table
203             ("<TABLE><TD BACKGROUND=\"javascript:alert('XSS')\">"),  # td
204             ("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">"),  # div background
205             ("<DIV STYLE=\"background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029\">"),  # div background with unicoded exploit
206             ("<DIV STYLE=\"background-image: url(&#1;javascript:alert('XSS'))\">"),  # div background + extra characters
207             ("<IMG SRC='vbscript:msgbox(\"XSS\")'>"),  # VBscrip in an image
208             ("<BODY ONLOAD=alert('XSS')>"),  # event handler
209             ("<BR SIZE=\"&{alert('XSS')}\>"),  # & javascript includes
210             ("<LINK REL=\"stylesheet\" HREF=\"javascript:alert('XSS');\">"),  # style sheet
211             ("<LINK REL=\"stylesheet\" HREF=\"http://ha.ckers.org/xss.css\">"),  # remote style sheet
212             ("<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>"),  # remote style sheet 2
213             ("<META HTTP-EQUIV=\"Link\" Content=\"<http://ha.ckers.org/xss.css>; REL=stylesheet\">"),  # remote style sheet 3
214             ("<STYLE>BODY{-moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\")}</STYLE>"),  # remote style sheet 4
215             ("<IMG STYLE=\"xss:expr/*XSS*/ession(alert('XSS'))\">"),  # style attribute using a comment to break up expression
216             ("""<!--[if gte IE 4]>
217                 <SCRIPT>alert('XSS');</SCRIPT>
218                 <![endif]-->"""),  # down-level hidden block
219         ]
220         for content in cases:
221             html = html_sanitize(content)
222             self.assertNotIn('javascript', html, 'html_sanitize did not remove a malicious javascript')
223             self.assertTrue('ha.ckers.org' not in html or 'http://ha.ckers.org/xss.css' in html, 'html_sanitize did not remove a malicious code in %s (%s)' % (content, html))
224
225     def test_html(self):
226         sanitized_html = html_sanitize(HTML_SOURCE)
227         for tag in ['<div', '<b', '<i', '<u', '<strike', '<li', '<blockquote', '<a href']:
228             self.assertIn(tag, sanitized_html, 'html_sanitize stripped too much of original html')
229         for attr in ['javascript']:
230             self.assertNotIn(attr, sanitized_html, 'html_sanitize did not remove enough unwanted attributes')
231
232         emails =[("Charles <charles.bidule@truc.fr>", "Charles &lt;charles.bidule@truc.fr&gt;"), 
233                 ("Dupuis <'tr/-: ${dupuis><#><$'@truc.baz.fr>", "Dupuis &lt;'tr/-: ${dupuis&gt;&lt;#&gt;&lt;$'@truc.baz.fr&gt;"),
234                 ("Technical <service/technical+2@open.com>", "Technical &lt;service/technical+2@open.com&gt;"),
235                 ("Div nico <div-nico@open.com>", "Div nico &lt;div-nico@open.com&gt;")]
236         for email in emails:
237             self.assertIn(email[1], html_sanitize(email[0]), 'html_sanitize stripped emails of original html')
238
239
240     def test_edi_source(self):
241         html = html_sanitize(EDI_LIKE_HTML_SOURCE)
242         self.assertIn('div style="font-family: \'Lucica Grande\', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF;', html,
243             'html_sanitize removed valid style attribute')
244         self.assertIn('<span style="color: #222; margin-bottom: 5px; display: block; ">', html,
245             'html_sanitize removed valid style attribute')
246         self.assertIn('img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"', html,
247             'html_sanitize removed valid img')
248         self.assertNotIn('</body></html>', html, 'html_sanitize did not remove extra closing tags')
249
250
251 class TestCleaner(unittest2.TestCase):
252     """ Test the email cleaner function that filters the content of incoming emails """
253
254     def test_html_email_clean(self):
255         # Test1: reply through gmail: quote in blockquote, signature --\nAdministrator
256         new_html = html_email_clean(GMAIL_REPLY1_SAN)
257         self.assertNotIn('blockquote', new_html, 'html_email_cleaner did not remove a blockquote')
258         self.assertNotIn('I contact you about our meeting', new_html, 'html_email_cleaner wrongly removed the quoted content')
259         self.assertNotIn('Administrator', new_html, 'html_email_cleaner did not erase the signature')
260         self.assertIn('Ok for me', new_html, 'html_email_cleaner erased too much content')
261
262         # Test2: reply through Tunderbird 16.0.2
263         new_html = html_email_clean(THUNDERBIRD_16_REPLY1_SAN)
264         self.assertNotIn('blockquote', new_html, 'html_email_cleaner did not remove a blockquote')
265         self.assertNotIn('I contact you about our meeting', new_html, 'html_email_cleaner wrongly removed the quoted content')
266         self.assertNotIn('Administrator', new_html, 'html_email_cleaner did not erase the signature')
267         self.assertNotIn('Grosbedonn', new_html, 'html_email_cleaner did not erase the signature')
268         self.assertIn('Ok for me', new_html, 'html_email_cleaner erased too much content')
269
270         # Test3: text email
271         new_html = html_email_clean(TEXT_MAIL1)
272         self.assertIn('I contact you about our meeting', new_html, 'html_email_cleaner wrongly removed the quoted content')
273         self.assertNotIn('Administrator', new_html, 'html_email_cleaner did not erase the signature')
274
275         # Test4: more complex text email
276         new_html = html_email_clean(TEXT_TPL)
277         self.assertNotIn('quote', new_html, 'html_email_cleaner did not remove correctly plaintext quotes')
278
279         # Test5: False boolean for text must return empty string
280         new_html = html_email_clean(False)
281         self.assertEqual(new_html, False, 'html_email_cleaner did change a False in an other value.')
282
283         # Test6: Message with xml and doctype tags don't crash
284         new_html = html_email_clean(u'<?xml version="1.0" encoding="iso-8859-1"?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">\n <head>\n  <title>404 - Not Found</title>\n </head>\n <body>\n  <h1>404 - Not Found</h1>\n </body>\n</html>\n')
285         self.assertNotIn('encoding', new_html, 'html_email_cleaner did not remove correctly encoding attributes')
286
287
288 class TestHtmlTools(unittest2.TestCase):
289     """ Test some of our generic utility functions about html """
290
291     def test_plaintext2html(self):
292         cases = [
293             ("First \nSecond \nThird\n \nParagraph\n\r--\nSignature paragraph", 'div',
294              "<div><p>First <br/>Second <br/>Third</p><p>Paragraph</p><p>--<br/>Signature paragraph</p></div>"),
295             ("First<p>It should be escaped</p>\nSignature", False,
296              "<p>First&lt;p&gt;It should be escaped&lt;/p&gt;<br/>Signature</p>")
297         ]
298         for content, container_tag, expected in cases:
299             html = plaintext2html(content, container_tag)
300             self.assertEqual(html, expected, 'plaintext2html is broken')
301
302     def test_append_to_html(self):
303         test_samples = [
304             ('<!DOCTYPE...><HTML encoding="blah">some <b>content</b></HtMl>', '--\nYours truly', True, True, False,
305              '<!DOCTYPE...><html encoding="blah">some <b>content</b>\n<pre>--\nYours truly</pre>\n</html>'),
306             ('<!DOCTYPE...><HTML encoding="blah">some <b>content</b></HtMl>', '--\nYours truly', True, False, False,
307              '<!DOCTYPE...><html encoding="blah">some <b>content</b>\n<p>--<br/>Yours truly</p>\n</html>'),
308             ('<html><body>some <b>content</b></body></html>', '<!DOCTYPE...>\n<html><body>\n<p>--</p>\n<p>Yours truly</p>\n</body>\n</html>', False, False, False,
309              '<html><body>some <b>content</b>\n\n\n<p>--</p>\n<p>Yours truly</p>\n\n\n</body></html>'),
310         ]
311         for html, content, plaintext_flag, preserve_flag, container_tag, expected in test_samples:
312             self.assertEqual(append_content_to_html(html, content, plaintext_flag, preserve_flag, container_tag), expected, 'append_content_to_html is broken')
313
314
315 if __name__ == '__main__':
316     unittest2.main()