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@bc237d1beb
This commit is contained in:
Brian Murphy 2015-04-17 09:45:16 -04:00
parent 771b8824a5
commit 3c7b42eb7b
3 changed files with 134 additions and 1 deletions

View File

@ -149,6 +149,12 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>r239</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
@ -328,6 +334,7 @@
<promoteTransitiveDependencies>true</promoteTransitiveDependencies>
<artifactSet>
<includes>
<include>com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer</include>
<include>org.quartz-scheduler:quartz</include>
<include>org.slf4j:slf4j-api</include>
<include>org.slf4j:slf4j-nop</include>

View File

@ -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<String, Attachment> 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<String, Attachment> attachments;
AttachementVerifyElementPolicy(ImmutableMap<String, Attachment> attchments) {
this.attachments = attchments;
}
@Nullable
@Override
public String apply(@ParametersAreNonnullByDefault String elementName, @ParametersAreNonnullByDefault List<String> 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;
}
}
}

View File

@ -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 = "<button type=\"button\"" +
"onclick=\"document.getElementById('demo').innerHTML = Date()\">" +
"Click me to display Date and Time.</button>";
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 = "<img src=\"http://test.com/nastyimage.jpg\"/>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 = "<img src=\"cid:foo\" />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 = "<table><tr><td>cell1</td><td>cell2</td></tr></table>";
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 = "<img src=\"cid:bad\" />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 = "<script>doSomethingNefarious()</script>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"));
}
}