From 3c7b42eb7b00470e626b0d9f11d5bc911f49dd41 Mon Sep 17 00:00:00 2001 From: Brian Murphy Date: Fri, 17 Apr 2015 09:45:16 -0400 Subject: [PATCH] Support for Sanitized HTML in emails. This change adds a shaded dependency on owasp (https://code.google.com/p/owasp-java-html-sanitizer/) to add support for HTML Sanitization. Only images that reference an attachment are supported. This Sanitization may be customized for each email profile. Other dangerous behavior is suppressed. See elastic/elasticsearch#163 Fixes elastic/elasticsearch#163 Original commit: elastic/x-pack-elasticsearch@bc237d1beb3d87f7b582748504475622fba72e6b --- pom.xml | 7 ++ .../actions/email/service/Profile.java | 58 ++++++++++++++- .../email/service/HtmlSanitizeTests.java | 70 +++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/elasticsearch/watcher/actions/email/service/HtmlSanitizeTests.java diff --git a/pom.xml b/pom.xml index 2bd118273e3..093a844ad2b 100644 --- a/pom.xml +++ b/pom.xml @@ -149,6 +149,12 @@ true + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + r239 + + org.apache.lucene lucene-core @@ -328,6 +334,7 @@ true + com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer org.quartz-scheduler:quartz org.slf4j:slf4j-api org.slf4j:slf4j-nop diff --git a/src/main/java/org/elasticsearch/watcher/actions/email/service/Profile.java b/src/main/java/org/elasticsearch/watcher/actions/email/service/Profile.java index 08513239d50..405041ddec0 100644 --- a/src/main/java/org/elasticsearch/watcher/actions/email/service/Profile.java +++ b/src/main/java/org/elasticsearch/watcher/actions/email/service/Profile.java @@ -6,9 +6,13 @@ package org.elasticsearch.watcher.actions.email.service; import org.elasticsearch.common.base.Charsets; +import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.owasp.html.*; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Session; @@ -16,6 +20,7 @@ import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import java.io.IOException; +import java.util.List; import java.util.Locale; /** @@ -87,7 +92,8 @@ public enum Profile implements ToXContent { if (email.htmlBody != null) { MimeBodyPart html = new MimeBodyPart(); - html.setText(email.htmlBody, Charsets.UTF_8.name(), "html"); + String sanitizedHtml = sanitizeHtml(email.attachments, email.htmlBody); + html.setText(sanitizedHtml, Charsets.UTF_8.name(), "html"); alternative.addBodyPart(html); } @@ -217,4 +223,54 @@ public enum Profile implements ToXContent { return part; } + static String sanitizeHtml(final ImmutableMap attachments, String html){ + ElementPolicy onlyCIDImgPolicy = new AttachementVerifyElementPolicy(attachments); + PolicyFactory policy = Sanitizers.FORMATTING + .and(new HtmlPolicyBuilder() + .allowElements("img", "table", "tr", "td", "style", "body", "head") + .allowAttributes("src").onElements("img") + .allowAttributes("class").onElements("style") + .allowUrlProtocols("cid") + .allowCommonInlineFormattingElements() + .allowElements(onlyCIDImgPolicy, "img") + .allowStyling(CssSchema.DEFAULT) + .toFactory()) + .and(Sanitizers.LINKS) + .and(Sanitizers.BLOCKS); + return policy.sanitize(html); + } + + + + private static class AttachementVerifyElementPolicy implements ElementPolicy { + + private final ImmutableMap attachments; + + AttachementVerifyElementPolicy(ImmutableMap attchments) { + this.attachments = attchments; + } + + @Nullable + @Override + public String apply(@ParametersAreNonnullByDefault String elementName, @ParametersAreNonnullByDefault List attrs) { + if (attrs.size() == 0) { + return elementName; + } + for (int i = 0; i < attrs.size(); ++i) { + if(attrs.get(i).equals("src") && i < attrs.size() - 1) { + String srcValue = attrs.get(i+1); + if (!srcValue.startsWith("cid:")) { + return null; //Disallow anything other than content ids + } + String contentId = srcValue.substring(4); + if (attachments.containsKey(contentId)) { + return elementName; + } else { + return null; //This cid wasn't found + } + } + } + return elementName; + } + } } diff --git a/src/test/java/org/elasticsearch/watcher/actions/email/service/HtmlSanitizeTests.java b/src/test/java/org/elasticsearch/watcher/actions/email/service/HtmlSanitizeTests.java new file mode 100644 index 00000000000..7d415a81525 --- /dev/null +++ b/src/test/java/org/elasticsearch/watcher/actions/email/service/HtmlSanitizeTests.java @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.watcher.actions.email.service; + +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; + +/** + */ +public class HtmlSanitizeTests extends ElasticsearchTestCase { + + @Test + public void test_HtmlSanitizer_onclick() { + String badHtml = ""; + byte[] bytes = new byte[0]; + String sanitizedHtml = Profile.sanitizeHtml(ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")), badHtml); + assertThat(sanitizedHtml, equalTo("Click me to display Date and Time.")); + } + + @Test + public void test_HtmlSanitizer_Nonattachment_img() { + String badHtml = "This is a bad image"; + byte[] bytes = new byte[0]; + String sanitizedHtml = Profile.sanitizeHtml(ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")), badHtml); + assertThat(sanitizedHtml, equalTo("This is a bad image")); + } + + @Test + public void test_HtmlSanitizer_Goodattachment_img() { + String goodHtml = "This is a good image"; + byte[] bytes = new byte[0]; + String sanitizedHtml = Profile.sanitizeHtml(ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")), goodHtml); + assertThat(sanitizedHtml, equalTo(goodHtml)); + } + + @Test + public void test_HtmlSanitizer_table() { + String goodHtml = "
cell1cell2
"; + byte[] bytes = new byte[0]; + String sanitizedHtml = Profile.sanitizeHtml(ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")), goodHtml); + assertThat(sanitizedHtml, equalTo(goodHtml)); + + } + + @Test + public void test_HtmlSanitizer_Badattachment_img() { + String goodHtml = "This is a bad image"; + byte[] bytes = new byte[0]; + String sanitizedHtml = Profile.sanitizeHtml(ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")), goodHtml); + assertThat(sanitizedHtml, equalTo("This is a bad image")); + } + + @Test + public void test_HtmlSanitizer_Script() { + String badHtml = "This was a dangerous script"; + byte[] bytes = new byte[0]; + String sanitizedHtml = Profile.sanitizeHtml(ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")), badHtml); + assertThat(sanitizedHtml, equalTo("This was a dangerous script")); + } + + +}