2 # -*- coding: utf-8 -*-
3 # This test can be run stand-alone with something like:
4 # > PYTHONPATH=. python2 openerp/tests/test_misc.py
5 ##############################################################################
7 # OpenERP, Open Source Business Applications
8 # Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
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.
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.
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/>.
23 ##############################################################################
26 from openerp.tools import html_sanitize, html_email_clean, append_content_to_html, plaintext2html
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>
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 <strong>REFERENCES</strong><br />
51 Invoice number: <strong>${object.number}</strong><br />
52 Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
53 Invoice date: ${object.date_invoice}<br />
54 Order reference: ${object.origin}<br />
55 Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
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"/>
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>
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>
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/>
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: ${object.company_id.phone}
82 Web : <a href="${object.company_id.website}">${object.company_id.website}</a>
85 </div></body></html>"""
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>
90 10 AM: meeting with Fabien to present our app
91 Is everything ok for you ?
96 <font><span>I contact you about our meeting for tomorrow. Here is the schedule I propose:</span></font>
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>
103 <div><font><span>Is everything ok for you ?</span></font></div>"""
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><<a href="mailto:dummy@example.com">dummy@example.com</a>></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</span></li></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>
109 <div><p>Log in our portal at: <a href="http://localhost:8069#action=login&db=mail_1&login=demo">http://localhost:8069#action=login&db=mail_1&login=demo</a></p></div>
110 </blockquote></div><br></div>"""
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>
115 <div>I contact you about our meeting for tomorrow. Here is the
116 schedule I propose:</div>
118 <ul><li>9 AM: brainstorming about our new amazing business
119 app</span></li></li>
120 <li>9.45 AM: summary</li>
121 <li>10 AM: meeting with Fabien to present our app</li>
123 <div>Is everything ok for you ?</div>
129 <p>Log in our portal at:
130 <a href="http://localhost:8069#action=login&db=mail_1&token=rHdWcUART5PhEnJRaXjH">http://localhost:8069#action=login&db=mail_1&token=rHdWcUART5PhEnJRaXjH</a></p>
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>
137 Raoul Grosbedonnée
140 TEXT_TPL = """Salut Raoul!
141 Le 28 oct. 2012 à 00:02, Raoul Grosbedon a écrit :
143 > C'est sûr que je suis intéressé (quote)!
145 Trouloulou pouet pouet. Je ne vais quand même pas écrire de vrais mails, non mais ho.
147 > 2012/10/27 Bert Tartopoils :
148 >> Diantre, me disè-je en envoyant un message similaire à Martine, mais comment vas-tu (quote)?
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)
152 >> Et sinon bon courage pour trouver tes clefs (quote).
155 >> bert.tartopoils@miam.miam
163 bert.tartopoils@miam.miam
167 class TestSanitizer(unittest2.TestCase):
168 """ Test the html sanitizer that filters html to remove unwanted attributes """
170 def test_basic_sanitizer(self):
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
177 for content, expected in cases:
178 html = html_sanitize(content)
179 self.assertEqual(html, expected, 'html_sanitize is broken')
181 def test_evil_malicious_code(self):
182 # taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Tests
184 ("<IMG SRC=javascript:alert('XSS')>"), # no quotes and semicolons
185 ("<IMG SRC=javascript:alert('XSS')>"), # UTF-8 Unicode encoding
186 ("<IMG SRC=javascript:alert('XSS')>"), # hex encoding
187 ("<IMG SRC=\"jav
ascript:alert('XSS');\">"), # embedded carriage return
188 ("<IMG SRC=\"jav
ascript:alert('XSS');\">"), # embedded newline
189 ("<IMG SRC=\"jav ascript:alert('XSS');\">"), # embedded tab
190 ("<IMG SRC=\"jav	ascript:alert('XSS');\">"), # embedded encoded tab
191 ("<IMG SRC=\"  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(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
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))
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')
232 emails =[("Charles <charles.bidule@truc.fr>", "Charles <charles.bidule@truc.fr>"),
233 ("Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>", "Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>"),
234 ("Technical <service/technical+2@open.com>", "Technical <service/technical+2@open.com>"),
235 ("Div nico <div-nico@open.com>", "Div nico <div-nico@open.com>")]
237 self.assertIn(email[1], html_sanitize(email[0]), 'html_sanitize stripped emails of original html')
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')
251 class TestCleaner(unittest2.TestCase):
252 """ Test the email cleaner function that filters the content of incoming emails """
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')
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')
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')
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')
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.')
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')
288 class TestHtmlTools(unittest2.TestCase):
289 """ Test some of our generic utility functions about html """
291 def test_plaintext2html(self):
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<p>It should be escaped</p><br/>Signature</p>")
298 for content, container_tag, expected in cases:
299 html = plaintext2html(content, container_tag)
300 self.assertEqual(html, expected, 'plaintext2html is broken')
302 def test_append_to_html(self):
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>'),
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')
315 if __name__ == '__main__':