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:
parent
771b8824a5
commit
3c7b42eb7b
7
pom.xml
7
pom.xml
|
@ -149,6 +149,12 @@
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||||
|
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||||
|
<version>r239</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.lucene</groupId>
|
<groupId>org.apache.lucene</groupId>
|
||||||
<artifactId>lucene-core</artifactId>
|
<artifactId>lucene-core</artifactId>
|
||||||
|
@ -328,6 +334,7 @@
|
||||||
<promoteTransitiveDependencies>true</promoteTransitiveDependencies>
|
<promoteTransitiveDependencies>true</promoteTransitiveDependencies>
|
||||||
<artifactSet>
|
<artifactSet>
|
||||||
<includes>
|
<includes>
|
||||||
|
<include>com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer</include>
|
||||||
<include>org.quartz-scheduler:quartz</include>
|
<include>org.quartz-scheduler:quartz</include>
|
||||||
<include>org.slf4j:slf4j-api</include>
|
<include>org.slf4j:slf4j-api</include>
|
||||||
<include>org.slf4j:slf4j-nop</include>
|
<include>org.slf4j:slf4j-nop</include>
|
||||||
|
|
|
@ -6,9 +6,13 @@
|
||||||
package org.elasticsearch.watcher.actions.email.service;
|
package org.elasticsearch.watcher.actions.email.service;
|
||||||
|
|
||||||
import org.elasticsearch.common.base.Charsets;
|
import org.elasticsearch.common.base.Charsets;
|
||||||
|
import org.elasticsearch.common.collect.ImmutableMap;
|
||||||
import org.elasticsearch.common.xcontent.ToXContent;
|
import org.elasticsearch.common.xcontent.ToXContent;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
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.Message;
|
||||||
import javax.mail.MessagingException;
|
import javax.mail.MessagingException;
|
||||||
import javax.mail.Session;
|
import javax.mail.Session;
|
||||||
|
@ -16,6 +20,7 @@ import javax.mail.internet.MimeBodyPart;
|
||||||
import javax.mail.internet.MimeMessage;
|
import javax.mail.internet.MimeMessage;
|
||||||
import javax.mail.internet.MimeMultipart;
|
import javax.mail.internet.MimeMultipart;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,7 +92,8 @@ public enum Profile implements ToXContent {
|
||||||
|
|
||||||
if (email.htmlBody != null) {
|
if (email.htmlBody != null) {
|
||||||
MimeBodyPart html = new MimeBodyPart();
|
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);
|
alternative.addBodyPart(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,4 +223,54 @@ public enum Profile implements ToXContent {
|
||||||
return part;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue