def _get_desc(self, cr, uid, ids, field_name=None, arg=None, context=None): res = dict.fromkeys(ids, '') for module in self.browse(cr, uid, ids, context=context): path = get_module_resource(module.name, 'static/description/index.html') if path: with tools.file_open(path, 'rb') as desc_file: doc = desc_file.read() html = lxml.html.document_fromstring(doc) for element, attribute, link, pos in html.iterlinks(): if element.get('src') and not '//' in element.get( 'src') and not 'static/' in element.get('src'): element.set( 'src', "/%s/static/description/%s" % (module.name, element.get('src'))) res[module.id] = html_sanitize(lxml.html.tostring(html)) else: overrides = { 'embed_stylesheet': False, 'doctitle_xform': False, 'output_encoding': 'unicode', 'xml_declaration': False, } output = publish_string(source=module.description or '', settings_overrides=overrides, writer=MyWriter()) res[module.id] = html_sanitize(output) return res
def test_html(self): sanitized_html = html_sanitize(test_mail_examples.MISC_HTML_SOURCE) for tag in ['<div', '<b', '<i', '<u', '<strike', '<li', '<blockquote', '<a href']: self.assertIn(tag, sanitized_html, 'html_sanitize stripped too much of original html') for attr in ['javascript']: self.assertNotIn(attr, sanitized_html, 'html_sanitize did not remove enough unwanted attributes') emails = [("Charles <*****@*****.**>", "Charles <[email protected]>"), ("Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>", "Dupuis <'tr/-: ${dupuis#$'@truc.baz.fr>"), ("Technical <service/[email protected]>", "Technical <service/[email protected]>"), ("Div nico <*****@*****.**>", "Div nico <[email protected]>")] for email in emails: self.assertIn(email[1], html_sanitize(email[0]), 'html_sanitize stripped emails of original html')
def convert_answer_to_comment(self): """ Tools to convert an answer (forum.post) to a comment (mail.message). The original post is unlinked and a new comment is posted on the question using the post create_uid as the comment's author. """ if not self.parent_id: return False # karma-based action check: use the post field that computed own/all value if not self.can_comment_convert: raise KarmaError('Not enough karma to convert an answer to a comment') # post the message question = self.parent_id values = { 'author_id': self.sudo().create_uid.partner_id.id, # use sudo here because of access to res.users model 'body': tools.html_sanitize(self.content, strict=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } new_message = self.browse(question.id).with_context(mail_create_nosubscribe=True).message_post(**values) # unlink the original answer, using SUPERUSER_ID to avoid karma issues self.sudo().unlink() return new_message
def test_edi_source(self): html = html_sanitize(test_mail_examples.EDI_LIKE_HTML_SOURCE) 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, 'html_sanitize removed valid style attribute') self.assertIn('<span style="color: #222; margin-bottom: 5px; display: block; ">', html, 'html_sanitize removed valid style attribute') self.assertIn('img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"', html, 'html_sanitize removed valid img') self.assertNotIn('</body></html>', html, 'html_sanitize did not remove extra closing tags')
def test_evil_malicious_code(self): # taken from https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Tests cases = [ ("<IMG SRC=javascript:alert('XSS')>"), # no quotes and semicolons ("<IMG SRC=javascript:alert('XSS')>"), # UTF-8 Unicode encoding ("<IMG SRC=javascript:alert('XSS')>"), # hex encoding ("<IMG SRC=\"jav
ascript:alert('XSS');\">"), # embedded carriage return ("<IMG SRC=\"jav
ascript:alert('XSS');\">"), # embedded newline ("<IMG SRC=\"jav ascript:alert('XSS');\">"), # embedded tab ("<IMG SRC=\"jav	ascript:alert('XSS');\">"), # embedded encoded tab ("<IMG SRC=\"  javascript:alert('XSS');\">"), # spaces and meta-characters ("<IMG SRC=\"javascript:alert('XSS')\""), # half-open html ("<IMG \"\"\"><SCRIPT>alert(\"XSS\")</SCRIPT>\">"), # malformed tag ("<SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"), # non-alpha-non-digits ("<SCRIPT/SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>"), # non-alpha-non-digits ("<<SCRIPT>alert(\"XSS\");//<</SCRIPT>"), # extraneous open brackets ("<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >"), # non-closing script tags ("<INPUT TYPE=\"IMAGE\" SRC=\"javascript:alert('XSS');\">"), # input image ("<BODY BACKGROUND=\"javascript:alert('XSS')\">"), # body image ("<IMG DYNSRC=\"javascript:alert('XSS')\">"), # img dynsrc ("<IMG LOWSRC=\"javascript:alert('XSS')\">"), # img lowsrc ("<TABLE BACKGROUND=\"javascript:alert('XSS')\">"), # table ("<TABLE><TD BACKGROUND=\"javascript:alert('XSS')\">"), # td ("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">"), # div background ("<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 ("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">"), # div background + extra characters ("<IMG SRC='vbscript:msgbox(\"XSS\")'>"), # VBscrip in an image ("<BODY ONLOAD=alert('XSS')>"), # event handler ("<BR SIZE=\"&{alert('XSS')}\>"), # & javascript includes ("<LINK REL=\"stylesheet\" HREF=\"javascript:alert('XSS');\">"), # style sheet ("<LINK REL=\"stylesheet\" HREF=\"http://ha.ckers.org/xss.css\">"), # remote style sheet ("<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>"), # remote style sheet 2 ("<META HTTP-EQUIV=\"Link\" Content=\"<http://ha.ckers.org/xss.css>; REL=stylesheet\">"), # remote style sheet 3 ("<STYLE>BODY{-moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\")}</STYLE>"), # remote style sheet 4 ("<IMG STYLE=\"xss:expr/*XSS*/ession(alert('XSS'))\">"), # style attribute using a comment to break up expression ] for content in cases: html = html_sanitize(content) self.assertNotIn('javascript', html, 'html_sanitize did not remove a malicious javascript') 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)) content = "<!--[if gte IE 4]><SCRIPT>alert('XSS');</SCRIPT><![endif]-->" # down-level hidden block self.assertEquals(html_sanitize(content, silent=False), '')
def test_basic_sanitizer(self): cases = [ ("yop", "<p>yop</p>"), # simple ("lala<p>yop</p>xxx", "<p>lala</p><p>yop</p>xxx"), # trailing text ("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci", u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"), # unicode ] for content, expected in cases: html = html_sanitize(content) self.assertEqual(html, expected, 'html_sanitize is broken')
def _get_desc(self, cr, uid, ids, field_name=None, arg=None, context=None): res = dict.fromkeys(ids, '') for module in self.browse(cr, uid, ids, context=context): path = get_module_resource(module.name, 'static/description/index.html') if path: with tools.file_open(path, 'rb') as desc_file: doc = desc_file.read() html = lxml.html.document_fromstring(doc) for element, attribute, link, pos in html.iterlinks(): if element.get('src') and not '//' in element.get('src') and not 'static/' in element.get('src'): element.set('src', "/%s/static/description/%s" % (module.name, element.get('src'))) res[module.id] = html_sanitize(lxml.html.tostring(html)) else: overrides = { 'embed_stylesheet': False, 'doctitle_xform': False, 'output_encoding': 'unicode', 'xml_declaration': False, } output = publish_string(source=module.description or '', settings_overrides=overrides, writer=MyWriter()) res[module.id] = html_sanitize(output) return res
def test_mako(self): cases = [ ('''<p>Some text</p> <% set signup_url = object.get_signup_url() %> % if signup_url: <p> You can access this document and pay online via our Customer Portal: </p>''', '''<p>Some text</p> <% set signup_url = object.get_signup_url() %> % if signup_url: <p> You can access this document and pay online via our Customer Portal: </p>''') ] for content, expected in cases: html = html_sanitize(content, silent=False) self.assertEqual(html, expected, 'html_sanitize: broken mako management')
def convert_answer_to_comment(self): """ Tools to convert an answer (forum.post) to a comment (mail.message). The original post is unlinked and a new comment is posted on the question using the post create_uid as the comment's author. """ if not self.parent_id: return False # karma-based action check: use the post field that computed own/all value if not self.can_comment_convert: raise KarmaError( 'Not enough karma to convert an answer to a comment') # post the message question = self.parent_id values = { 'author_id': self.sudo().create_uid.partner_id. id, # use sudo here because of access to res.users model 'body': tools.html_sanitize(self.content, strict=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } new_message = self.browse(question.id).with_context( mail_create_nosubscribe=True).message_post(**values) # unlink the original answer, using SUPERUSER_ID to avoid karma issues self.sudo().unlink() return new_message
def generate_email(self, res_ids, fields=None): """Generates an email from the template for given the given model based on records given by res_ids. :param template_id: id of the template to render. :param res_id: id of the record to use for rendering the template (model is taken from template definition) :returns: a dict containing all relevant fields for creating a new mail.mail entry, with one extra key ``attachments``, in the format [(report_name, data)] where data is base64 encoded. """ self.ensure_one() multi_mode = True if isinstance(res_ids, (int, long)): res_ids = [res_ids] multi_mode = False if fields is None: fields = [ 'subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to' ] res_ids_to_templates = self.get_email_template_batch(res_ids) # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} for res_id, template in res_ids_to_templates.iteritems(): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() for template, template_res_ids in templates_to_res_ids.iteritems(): Template = self.env['mail.template'] # generate fields value for all res_ids linked to the current template if template.lang: Template = Template.with_context( lang=template._context.get('lang')) for field in fields: Template = Template.with_context(safe=field in {'subject'}) generated_field_values = Template.render_template( getattr(template, field), template.model, template_res_ids, post_process=(field == 'body_html')) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value # compute recipients if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']): results = template.generate_recipients(results, template_res_ids) # update values for all res_ids for res_id in template_res_ids: values = results[res_id] # body: add user signature, sanitize if 'body_html' in fields and template.user_signature: signature = self.env.user.signature if signature: values['body_html'] = tools.append_content_to_html( values['body_html'], signature, plaintext=False) if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) # technical settings values.update( mail_server_id=template.mail_server_id.id or False, auto_delete=template.auto_delete, model=template.model, res_id=res_id or False, attachment_ids=[ attach.id for attach in template.attachment_ids ], ) # Add report in attachments: generate once for all template_res_ids if template.report_template and not 'report_template_in_attachment' in self.env.context: for res_id in template_res_ids: attachments = [] report_name = self.render_template(template.report_name, template.model, res_id) report = template.report_template report_service = report.report_name if report.report_type in ['qweb-html', 'qweb-pdf']: result, format = self.pool['report'].get_pdf( self._cr, self._uid, [res_id], report_service, context=Template._context), 'pdf' else: result, format = ecore_report.render_report( self._cr, self._uid, [res_id], report_service, {'model': template.model}, Template._context) # TODO in trunk, change return format to binary to match message_post expected format result = base64.b64encode(result) if not report_name: report_name = 'report.' + report_service ext = "." + format if not report_name.endswith(ext): report_name += ext attachments.append((report_name, result)) results[res_id]['attachments'] = attachments return multi_mode and results or results[res_ids[0]]