Made email html body sanitization configurable

Until now the email sanitization was fixed and could not be configured. That means that if ppl wanted a feature and our sanitization didn't support it, they were forced to disable sanitizaion all together.

In this commit a new `HtmlSanitizer` construct was introduced that is bound by Guice and can be configured via the settings.

Closes elastic/elasticsearch#586

Original commit: elastic/x-pack-elasticsearch@0081d1bf41
This commit is contained in:
uboness 2015-06-14 18:28:52 +02:00
parent b6e8df6a32
commit 09fcecc069
12 changed files with 384 additions and 214 deletions

View File

@ -10,6 +10,7 @@ import org.elasticsearch.common.inject.multibindings.MapBinder;
import org.elasticsearch.watcher.actions.email.EmailAction;
import org.elasticsearch.watcher.actions.email.EmailActionFactory;
import org.elasticsearch.watcher.actions.email.service.EmailService;
import org.elasticsearch.watcher.actions.email.service.HtmlSanitizer;
import org.elasticsearch.watcher.actions.email.service.InternalEmailService;
import org.elasticsearch.watcher.actions.index.IndexAction;
import org.elasticsearch.watcher.actions.index.IndexActionFactory;
@ -54,6 +55,7 @@ public class ActionModule extends AbstractModule {
}
bind(ActionRegistry.class).asEagerSingleton();
bind(HtmlSanitizer.class).asEagerSingleton();
bind(EmailService.class).to(InternalEmailService.class).asEagerSingleton();
}

View File

@ -113,8 +113,8 @@ public class EmailAction implements Action {
return builder.endObject();
}
public static EmailAction parse(String watchId, String actionId, XContentParser parser, boolean sanitizeHtmlBody) throws IOException {
EmailTemplate.Parser emailParser = new EmailTemplate.Parser(sanitizeHtmlBody);
public static EmailAction parse(String watchId, String actionId, XContentParser parser) throws IOException {
EmailTemplate.Parser emailParser = new EmailTemplate.Parser();
String account = null;
String user = null;
Secret password = null;

View File

@ -9,10 +9,9 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.watcher.actions.Action;
import org.elasticsearch.watcher.actions.ActionFactory;
import org.elasticsearch.watcher.actions.email.service.EmailService;
import org.elasticsearch.watcher.execution.Wid;
import org.elasticsearch.watcher.actions.email.service.HtmlSanitizer;
import org.elasticsearch.watcher.support.template.TemplateEngine;
import java.io.IOException;
@ -24,16 +23,14 @@ public class EmailActionFactory extends ActionFactory<EmailAction, ExecutableEma
private final EmailService emailService;
private final TemplateEngine templateEngine;
private final boolean sanitizeHtmlBodyOfEmails;
private static String SANITIZE_HTML_SETTING = "watcher.actions.email.sanitize_html";
private final HtmlSanitizer htmlSanitizer;
@Inject
public EmailActionFactory(Settings settings, EmailService emailService, TemplateEngine templateEngine) {
public EmailActionFactory(Settings settings, EmailService emailService, TemplateEngine templateEngine, HtmlSanitizer htmlSanitizer) {
super(Loggers.getLogger(ExecutableEmailAction.class, settings));
this.emailService = emailService;
this.templateEngine = templateEngine;
sanitizeHtmlBodyOfEmails = settings.getAsBoolean(SANITIZE_HTML_SETTING, true);
this.htmlSanitizer = htmlSanitizer;
}
@Override
@ -43,11 +40,11 @@ public class EmailActionFactory extends ActionFactory<EmailAction, ExecutableEma
@Override
public EmailAction parseAction(String watchId, String actionId, XContentParser parser) throws IOException {
return EmailAction.parse(watchId, actionId, parser, sanitizeHtmlBodyOfEmails);
return EmailAction.parse(watchId, actionId, parser);
}
@Override
public ExecutableEmailAction createExecutable(EmailAction action) {
return new ExecutableEmailAction(action, actionLogger, emailService, templateEngine);
return new ExecutableEmailAction(action, actionLogger, emailService, templateEngine, htmlSanitizer);
}
}

View File

@ -11,6 +11,7 @@ import org.elasticsearch.watcher.actions.ExecutableAction;
import org.elasticsearch.watcher.actions.email.service.Attachment;
import org.elasticsearch.watcher.actions.email.service.Email;
import org.elasticsearch.watcher.actions.email.service.EmailService;
import org.elasticsearch.watcher.actions.email.service.HtmlSanitizer;
import org.elasticsearch.watcher.execution.WatchExecutionContext;
import org.elasticsearch.watcher.support.Variables;
import org.elasticsearch.watcher.support.template.TemplateEngine;
@ -25,11 +26,13 @@ public class ExecutableEmailAction extends ExecutableAction<EmailAction> {
final EmailService emailService;
final TemplateEngine templateEngine;
final HtmlSanitizer htmlSanitizer;
public ExecutableEmailAction(EmailAction action, ESLogger logger, EmailService emailService, TemplateEngine templateEngine) {
public ExecutableEmailAction(EmailAction action, ESLogger logger, EmailService emailService, TemplateEngine templateEngine, HtmlSanitizer htmlSanitizer) {
super(action, logger);
this.emailService = emailService;
this.templateEngine = templateEngine;
this.htmlSanitizer = htmlSanitizer;
}
public Action.Result execute(String actionId, WatchExecutionContext ctx, Payload payload) throws Exception {
@ -42,7 +45,7 @@ public class ExecutableEmailAction extends ExecutableAction<EmailAction> {
attachments.put(attachment.id(), attachment);
}
Email.Builder email = action.getEmail().render(templateEngine, model, attachments);
Email.Builder email = action.getEmail().render(templateEngine, model, htmlSanitizer, attachments);
email.id(ctx.id().value());
if (ctx.simulateAction(actionId)) {

View File

@ -11,9 +11,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.watcher.WatcherException;
import org.elasticsearch.watcher.support.template.Template;
import org.elasticsearch.watcher.support.template.TemplateEngine;
import org.owasp.html.*;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.mail.internet.AddressException;
import java.io.IOException;
import java.util.*;
@ -32,11 +30,10 @@ public class EmailTemplate implements ToXContent {
final Template subject;
final Template textBody;
final Template htmlBody;
final boolean sanitizeHtmlBody;
public EmailTemplate(Template from, Template[] replyTo, Template priority, Template[] to,
Template[] cc, Template[] bcc, Template subject, Template textBody,
Template htmlBody, boolean sanitizeHtmlBody) {
Template htmlBody) {
this.from = from;
this.replyTo = replyTo;
this.priority = priority;
@ -46,7 +43,6 @@ public class EmailTemplate implements ToXContent {
this.subject = subject;
this.textBody = textBody;
this.htmlBody = htmlBody;
this.sanitizeHtmlBody = sanitizeHtmlBody;
}
public Template from() {
@ -85,11 +81,7 @@ public class EmailTemplate implements ToXContent {
return htmlBody;
}
public boolean sanitizeHtmlBody() {
return sanitizeHtmlBody;
}
public Email.Builder render(TemplateEngine engine, Map<String, Object> model, Map<String, Attachment> attachments) throws AddressException {
public Email.Builder render(TemplateEngine engine, Map<String, Object> model, HtmlSanitizer htmlSanitizer, Map<String, Attachment> attachments) throws AddressException {
Email.Builder builder = Email.builder();
if (from != null) {
builder.from(engine.render(from, model));
@ -126,9 +118,7 @@ public class EmailTemplate implements ToXContent {
}
if (htmlBody != null) {
String renderedHtml = engine.render(htmlBody, model);
if (sanitizeHtmlBody && htmlBody != null) {
renderedHtml = sanitizeHtml(renderedHtml, attachments);
}
renderedHtml = htmlSanitizer.sanitize(renderedHtml);
builder.htmlBody(renderedHtml);
}
return builder;
@ -236,7 +226,6 @@ public class EmailTemplate implements ToXContent {
private Template subject;
private Template textBody;
private Template htmlBody;
private boolean sanitizeHtmlBody = true;
private Builder() {
}
@ -377,81 +366,27 @@ public class EmailTemplate implements ToXContent {
return this;
}
public Builder htmlBody(String html, boolean sanitizeHtmlBody) {
return htmlBody(Template.defaultType(html), sanitizeHtmlBody);
public Builder htmlBody(String html) {
return htmlBody(Template.defaultType(html));
}
public Builder htmlBody(Template.Builder html, boolean sanitizeHtmlBody) {
return htmlBody(html.build(), sanitizeHtmlBody);
public Builder htmlBody(Template.Builder html) {
return htmlBody(html.build());
}
public Builder htmlBody(Template html, boolean sanitizeHtmlBody) {
public Builder htmlBody(Template html) {
this.htmlBody = html;
this.sanitizeHtmlBody = sanitizeHtmlBody;
return this;
}
public EmailTemplate build() {
return new EmailTemplate(from, replyTo, priority, to, cc, bcc, subject, textBody, htmlBody, sanitizeHtmlBody);
}
}
static String sanitizeHtml(String html, final Map<String, Attachment> attachments){
ElementPolicy onlyCIDImgPolicy = new AttachementVerifyElementPolicy(attachments);
PolicyFactory policy = Sanitizers.FORMATTING
.and(new HtmlPolicyBuilder()
.allowElements("img", "table", "tr", "td", "style", "body", "head", "hr")
.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 Map<String, Attachment> attachments;
AttachementVerifyElementPolicy(Map<String, Attachment> attachments) {
this.attachments = attachments;
}
@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;
return new EmailTemplate(from, replyTo, priority, to, cc, bcc, subject, textBody, htmlBody);
}
}
public static class Parser {
private final EmailTemplate.Builder builder = builder();
private final boolean sanitizeHtmlBody;
public Parser(boolean sanitizeHtmlBody) {
this.sanitizeHtmlBody = sanitizeHtmlBody;
}
public boolean handle(String fieldName, XContentParser parser) throws IOException {
if (Email.Field.FROM.match(fieldName)) {
@ -514,7 +449,7 @@ public class EmailTemplate implements ToXContent {
} else if (Email.Field.BODY_TEXT.match(currentFieldName)) {
builder.textBody(Template.parse(parser));
} else if (Email.Field.BODY_HTML.match(currentFieldName)) {
builder.htmlBody(Template.parse(parser), sanitizeHtmlBody);
builder.htmlBody(Template.parse(parser));
} else {
throw new ParseException("could not parse email template. unknown field [{}.{}] field", fieldName, currentFieldName);
}

View File

@ -0,0 +1,189 @@
/*
* 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.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.owasp.html.CssSchema;
import org.owasp.html.ElementPolicy;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
/**
*
*/
public class HtmlSanitizer {
static final String[] FORMATTING_TAGS = new String[] {
"b", "i", "s", "u", "o", "sup", "sub", "ins", "del", "strong",
"strike", "tt", "code", "big", "small", "br", "span", "em"
};
static final String[] BLOCK_TAGS = new String[] {
"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "blockquote"
};
static final String[] TABLE_TAGS = new String[] {
"table", "hr", "tr", "td"
};
static final String[] DEFAULT_ALLOWED = new String[] {
"body", "head", "_tables", "_links", "_blocks", "_formatting", "img:embedded"
};
private final boolean enabled;
private final PolicyFactory policy;
@Inject
public HtmlSanitizer(Settings settings) {
enabled = settings.getAsBoolean("watcher.actions.email.html.sanitization.enabled", true);
String[] allow = settings.getAsArray("watcher.actions.email.html.sanitization.allow", DEFAULT_ALLOWED);
String[] disallow = settings.getAsArray("watcher.actions.email.html.sanitization.disallow", Strings.EMPTY_ARRAY);
policy = createCommonPolicy(allow, disallow);
}
public String sanitize(String html) {
if (!enabled) {
return html;
}
return policy.sanitize(html);
}
static PolicyFactory createCommonPolicy(String[] allow, String[] disallow) {
HtmlPolicyBuilder policyBuilder = new HtmlPolicyBuilder();
if (Arrays.binarySearch(allow, "_all") > -1) {
return policyBuilder
.allowElements(TABLE_TAGS)
.allowElements(BLOCK_TAGS)
.allowElements(FORMATTING_TAGS)
.allowStyling(CssSchema.DEFAULT)
.allowStandardUrlProtocols().allowElements("a")
.allowAttributes("href").onElements("a").requireRelNofollowOnLinks()
.allowElements("img")
.allowAttributes("src").onElements("img")
.allowStandardUrlProtocols()
.allowUrlProtocols("cid")
.toFactory();
}
EnumSet<Images> images = EnumSet.noneOf(Images.class);
for (String tag : allow) {
tag = tag.toLowerCase(Locale.ROOT);
switch (tag) {
case "_tables":
policyBuilder.allowElements(TABLE_TAGS);
break;
case "_links":
policyBuilder.allowElements("a")
.allowAttributes("href").onElements("a")
.allowStandardUrlProtocols()
.requireRelNofollowOnLinks();
break;
case "_blocks":
policyBuilder.allowElements(BLOCK_TAGS);
break;
case "_formatting":
policyBuilder.allowElements(FORMATTING_TAGS);
break;
case "_styles":
policyBuilder.allowStyling(CssSchema.DEFAULT);
break;
case "img:all":
case "img":
images.add(Images.ALL);
break;
case "img:embedded":
images.add(Images.EMBEDDED);
break;
default:
policyBuilder.allowElements(tag);
}
}
for (String tag : disallow) {
tag = tag.toLowerCase(Locale.ROOT);
switch (tag) {
case "_tables":
policyBuilder.disallowElements(TABLE_TAGS);
break;
case "_links":
policyBuilder.disallowElements("a");
break;
case "_blocks":
policyBuilder.disallowElements(BLOCK_TAGS);
break;
case "_formatting":
policyBuilder.disallowElements(FORMATTING_TAGS);
break;
case "_styles":
policyBuilder.disallowAttributes("style");
break;
case "img:all":
case "img":
images.remove(Images.ALL);
break;
case "img:embedded":
images.remove(Images.EMBEDDED);
break;
default:
policyBuilder.disallowElements(tag);
}
}
if (!images.isEmpty()) {
policyBuilder.allowAttributes("src").onElements("img").allowUrlProtocols("cid");
if (images.contains(Images.ALL)) {
policyBuilder.allowElements("img");
policyBuilder.allowStandardUrlProtocols();
} else {
// embedded
policyBuilder.allowElements(EmbeddedImgOnlyPolicy.INSTANCE, "img");
}
}
return policyBuilder.toFactory();
}
/**
* An {@code img} tag policy that only accept {@code cid:} values in its {@code src} attribute.
* If such value is found, the content id is verified against the available attachements of the
* email and if the content/attachment is not found, the element is dropped.
*/
private static class EmbeddedImgOnlyPolicy implements ElementPolicy {
private static EmbeddedImgOnlyPolicy INSTANCE = new EmbeddedImgOnlyPolicy();
@Override
public String apply(String elementName, List<String> attrs) {
if (!"img".equals(elementName) || attrs.size() == 0) {
return elementName;
}
String attrName = null;
for (String attr : attrs) {
if (attrName == null) {
attrName = attr.toLowerCase(Locale.ROOT);
continue;
}
// reject external image source (only allow embedded ones)
if ("src".equals(attrName) && !attr.startsWith("cid:")) {
return null;
}
}
return elementName;
}
}
enum Images {
ALL,
EMBEDDED
}
}

View File

@ -6,7 +6,6 @@
package org.elasticsearch.watcher.actions.email;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import com.carrotsearch.randomizedtesting.annotations.Seed;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.collect.MapBuilder;
@ -58,6 +57,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
}
};
TemplateEngine engine = mock(TemplateEngine.class);
HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class);
EmailTemplate.Builder emailBuilder = EmailTemplate.builder();
Template subject = null;
@ -73,7 +73,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
Template htmlBody = null;
if (randomBoolean()) {
htmlBody = Template.inline("_html_body").build();
emailBuilder.htmlBody(htmlBody, true);
emailBuilder.htmlBody(htmlBody);
}
EmailTemplate email = emailBuilder.build();
@ -83,7 +83,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
DataAttachment dataAttachment = randomDataAttachment();
EmailAction action = new EmailAction(email, account, auth, profile, dataAttachment);
ExecutableEmailAction executable = new ExecutableEmailAction(action, logger, service, engine);
ExecutableEmailAction executable = new ExecutableEmailAction(action, logger, service, engine, htmlSanitizer);
Map<String, Object> data = new HashMap<>();
Payload payload = new Payload.Simple(data);
@ -121,6 +121,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
when(engine.render(textBody, expectedModel)).thenReturn(textBody.getTemplate());
}
if (htmlBody != null) {
when(htmlSanitizer.sanitize(htmlBody.getTemplate())).thenReturn(htmlBody.getTemplate());
when(engine.render(htmlBody, expectedModel)).thenReturn(htmlBody.getTemplate());
}
@ -143,6 +144,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
@Test @Repeat(iterations = 20)
public void testParser() throws Exception {
TemplateEngine engine = mock(TemplateEngine.class);
HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class);
EmailService emailService = mock(EmailService.class);
Profile profile = randomFrom(Profile.values());
Email.Priority priority = randomFrom(Email.Priority.values());
@ -242,7 +244,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
ExecutableEmailAction executable = new EmailActionFactory(ImmutableSettings.EMPTY, emailService, engine)
ExecutableEmailAction executable = new EmailActionFactory(ImmutableSettings.EMPTY, emailService, engine, htmlSanitizer)
.parseExecutable(randomAsciiOfLength(8), randomAsciiOfLength(3), parser);
assertThat(executable, notNullValue());
@ -290,6 +292,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
public void testParser_SelfGenerated() throws Exception {
EmailService service = mock(EmailService.class);
TemplateEngine engine = mock(TemplateEngine.class);
HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class);
EmailTemplate.Builder emailTemplate = EmailTemplate.builder();
if (randomBoolean()) {
emailTemplate.from("from@domain");
@ -313,7 +316,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
emailTemplate.textBody("_text_body");
}
if (randomBoolean()) {
emailTemplate.htmlBody("_html_body", randomBoolean());
emailTemplate.htmlBody("_html_body");
}
EmailTemplate email = emailTemplate.build();
Authentication auth = randomBoolean() ? null : new Authentication("_user", new Secret("_passwd".toCharArray()));
@ -322,7 +325,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
DataAttachment dataAttachment = randomDataAttachment();
EmailAction action = new EmailAction(email, account, auth, profile, dataAttachment);
ExecutableEmailAction executable = new ExecutableEmailAction(action, logger, service, engine);
ExecutableEmailAction executable = new ExecutableEmailAction(action, logger, service, engine, htmlSanitizer);
boolean hideSecrets = randomBoolean();
ToXContent.Params params = WatcherParams.builder().hideSecrets(hideSecrets).build();
@ -333,7 +336,7 @@ public class EmailActionTests extends ElasticsearchTestCase {
logger.info(bytes.toUtf8());
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
ExecutableEmailAction parsed = new EmailActionFactory(ImmutableSettings.EMPTY, service, engine)
ExecutableEmailAction parsed = new EmailActionFactory(ImmutableSettings.EMPTY, service, engine, htmlSanitizer)
.parseExecutable(randomAsciiOfLength(4), randomAsciiOfLength(10), parser);
if (!hideSecrets) {
@ -358,10 +361,11 @@ public class EmailActionTests extends ElasticsearchTestCase {
public void testParser_Invalid() throws Exception {
EmailService emailService = mock(EmailService.class);
TemplateEngine engine = mock(TemplateEngine.class);
HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class);
XContentBuilder builder = jsonBuilder().startObject().field("unknown_field", "value");
XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes());
parser.nextToken();
new EmailActionFactory(ImmutableSettings.EMPTY, emailService, engine)
new EmailActionFactory(ImmutableSettings.EMPTY, emailService, engine, htmlSanitizer)
.parseExecutable(randomAsciiOfLength(3), randomAsciiOfLength(7), parser);
}

View File

@ -43,25 +43,18 @@ public class EmailTemplateTests extends ElasticsearchTestCase {
Template[] cc = randomFrom(possibleList, null);
Template[] bcc = randomFrom(possibleList, null);
Template priority = Template.inline(randomFrom(Email.Priority.values()).name()).build();
boolean sanitizeHtml = randomBoolean();
Template templatedSubject = Template.inline("Templated Subject {{foo}}").build();
String renderedTemplatedSubject = "Templated Subject bar";
Template subjectTemplate = Template.inline("Templated Subject {{foo}}").build();
String subject = "Templated Subject bar";
Template templatedBody = Template.inline("Templated Body {{foo}}").build();
String renderedTemplatedBody = "Templated Body bar";
Template textBodyTemplate = Template.inline("Templated Body {{foo}}").build();
String textBody = "Templated Body bar";
Template templatedHtmlBodyGood = Template.inline("Templated Html Body <hr />").build();
String renderedTemplatedHtmlBodyGood = "Templated Html Body <hr /> bar";
Template htmlBodyTemplate = Template.inline("Templated Html Body <script>nefarious scripting</script>").build();
String htmlBody = "Templated Html Body <script>nefarious scripting</script>";
String sanitizedHtmlBody = "Templated Html Body";
Template templatedHtmlBodyBad = Template.inline("Templated Html Body <script>nefarious scripting</script>").build();
String renderedTemplatedHtmlBodyBad = "Templated Html Body<script>nefarious scripting</script>";
String renderedSanitizedHtmlBodyBad = "Templated Html Body";
Template htmlBody = randomFrom(templatedHtmlBodyGood, templatedHtmlBodyBad);
EmailTemplate emailTemplate = new EmailTemplate(from, replyTo, priority, to, cc, bcc,
templatedSubject, templatedBody, htmlBody, sanitizeHtml);
EmailTemplate emailTemplate = new EmailTemplate(from, replyTo, priority, to, cc, bcc, subjectTemplate, textBodyTemplate, htmlBodyTemplate);
XContentBuilder builder = XContentFactory.jsonBuilder();
emailTemplate.toXContent(builder, ToXContent.EMPTY_PARAMS);
@ -69,7 +62,7 @@ public class EmailTemplateTests extends ElasticsearchTestCase {
XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes());
parser.nextToken();
EmailTemplate.Parser emailTemplateParser = new EmailTemplate.Parser(sanitizeHtml);
EmailTemplate.Parser emailTemplateParser = new EmailTemplate.Parser();
String currentFieldName = null;
XContentParser.Token token;
@ -83,11 +76,14 @@ public class EmailTemplateTests extends ElasticsearchTestCase {
EmailTemplate parsedEmailTemplate = emailTemplateParser.parsedTemplate();
Map<String, Object> model = new HashMap<>();
HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class);
when(htmlSanitizer.sanitize(htmlBody)).thenReturn(sanitizedHtmlBody);
TemplateEngine templateEngine = mock(TemplateEngine.class);
when(templateEngine.render(templatedSubject, model)).thenReturn(renderedTemplatedSubject);
when(templateEngine.render(templatedBody, model)).thenReturn(renderedTemplatedBody);
when(templateEngine.render(templatedHtmlBodyGood, model)).thenReturn(renderedTemplatedHtmlBodyGood);
when(templateEngine.render(templatedHtmlBodyBad, model)).thenReturn(renderedTemplatedHtmlBodyBad);
when(templateEngine.render(subjectTemplate, model)).thenReturn(subject);
when(templateEngine.render(textBodyTemplate, model)).thenReturn(textBody);
when(templateEngine.render(htmlBodyTemplate, model)).thenReturn(htmlBody);
for (Template possibleAddress : possibleList) {
when(templateEngine.render(possibleAddress, model)).thenReturn(possibleAddress.getTemplate());
}
@ -96,7 +92,7 @@ public class EmailTemplateTests extends ElasticsearchTestCase {
}
when(templateEngine.render(priority, model)).thenReturn(priority.getTemplate());
Email.Builder emailBuilder = parsedEmailTemplate.render(templateEngine, model, new HashMap<String, Attachment>());
Email.Builder emailBuilder = parsedEmailTemplate.render(templateEngine, model, htmlSanitizer, new HashMap<String, Attachment>());
assertThat(emailTemplate.from, equalTo(parsedEmailTemplate.from));
assertThat(emailTemplate.replyTo, equalTo(parsedEmailTemplate.replyTo));
@ -110,17 +106,9 @@ public class EmailTemplateTests extends ElasticsearchTestCase {
emailBuilder.id("_id");
Email email = emailBuilder.build();
assertThat(email.subject, equalTo(renderedTemplatedSubject));
assertThat(email.textBody, equalTo(renderedTemplatedBody));
if (htmlBody.equals(templatedHtmlBodyBad)) {
if (sanitizeHtml) {
assertThat(email.htmlBody, equalTo(renderedSanitizedHtmlBodyBad));
} else {
assertThat(email.htmlBody, equalTo(renderedTemplatedHtmlBodyBad));
}
} else {
assertThat(email.htmlBody, equalTo(renderedTemplatedHtmlBodyGood));
}
assertThat(email.subject, equalTo(subject));
assertThat(email.textBody, equalTo(textBody));
assertThat(email.htmlBody, equalTo(sanitizedHtmlBody));
}

View File

@ -1,79 +0,0 @@
/*
* 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.elasticsearch.watcher.actions.email.service.EmailTemplate.sanitizeHtml;
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 = sanitizeHtml(badHtml, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
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 = sanitizeHtml(badHtml, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
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 = sanitizeHtml(goodHtml, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
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 = sanitizeHtml(goodHtml, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
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 = sanitizeHtml(goodHtml, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
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 = sanitizeHtml(badHtml, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
assertThat(sanitizedHtml, equalTo("This was a dangerous script"));
}
@Test
public void test_HtmlSanitizer_FullHtmlWithMetaString() {
String needsSanitation = "<html><head></head><body><h1>Hello {{ctx.metadata.name}}</h1> meta <a href='https://www.google.com/search?q={{ctx.metadata.name}}'>Testlink</a>meta</body></html>";
byte[] bytes = new byte[0];
String sanitizedHtml = sanitizeHtml(needsSanitation, ImmutableMap.of("foo", (Attachment) new Attachment.Bytes("foo", bytes, "")));
assertThat(sanitizedHtml, equalTo("<head></head><body><h1>Hello {{ctx.metadata.name}}</h1> meta <a href=\"https://www.google.com/search?q&#61;{{ctx.metadata.name}}\" rel=\"nofollow\">Testlink</a>meta</body>"));
}
}

View File

@ -0,0 +1,131 @@
/*
* 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 com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
/**
*
*/
public class HtmlSanitizerTests extends ElasticsearchTestCase {
@Test @Repeat(iterations = 20)
public void testDefault_WithTemplatePlaceholders() {
String blockTag = randomFrom(HtmlSanitizer.BLOCK_TAGS);
while (blockTag.equals("li")) {
blockTag = randomFrom(HtmlSanitizer.BLOCK_TAGS);
}
String html =
"<html>" +
"<head></head>" +
"<body>" +
"<" + blockTag + ">Hello {{ctx.metadata.name}}</" + blockTag + ">" +
"<ul><li>item1</li></ul>" +
"<ol><li>item2</li></ol>" +
"meta <a href='https://www.google.com/search?q={{ctx.metadata.name}}'>Testlink</a> meta" +
"</body>" +
"</html>";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.EMPTY);
String sanitizedHtml = sanitizer.sanitize(html);
if (blockTag.equals("ol") || blockTag.equals("ul")) {
assertThat(sanitizedHtml, equalTo(
"<head></head><body>" +
"<" + blockTag + "><li>Hello {{ctx.metadata.name}}</li></" + blockTag + ">" +
"<ul><li>item1</li></ul>" +
"<ol><li>item2</li></ol>" +
"meta <a href=\"https://www.google.com/search?q&#61;{{ctx.metadata.name}}\" rel=\"nofollow\">Testlink</a> meta" +
"</body>"));
} else {
assertThat(sanitizedHtml, equalTo(
"<head></head><body>" +
"<" + blockTag + ">Hello {{ctx.metadata.name}}</" + blockTag + ">" +
"<ul><li>item1</li></ul>" +
"<ol><li>item2</li></ol>" +
"meta <a href=\"https://www.google.com/search?q&#61;{{ctx.metadata.name}}\" rel=\"nofollow\">Testlink</a> meta" +
"</body>"));
}
}
@Test
public void testDefault_onclick_Disallowed() {
String badHtml = "<button type=\"button\"" +
"onclick=\"document.getElementById('demo').innerHTML = Date()\">" +
"Click me to display Date and Time.</button>";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.EMPTY);
String sanitizedHtml = sanitizer.sanitize(badHtml);
assertThat(sanitizedHtml, equalTo("Click me to display Date and Time."));
}
@Test
public void testDefault_ExternalImage_Disallowed() {
String html = "<img src=\"http://test.com/nastyimage.jpg\"/>This is a bad image";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.EMPTY);
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo("This is a bad image"));
}
@Test
public void testDefault_EmbeddedImage_Allowed() {
String html = "<img src=\"cid:foo\" />This is a good image";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.EMPTY);
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo(html));
}
@Test
public void testDefault_Tables_Allowed() {
String html = "<table><tr><td>cell1</td><td>cell2</td></tr></table>";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.EMPTY);
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo(html));
}
@Test
public void testDefault_Scipts_Disallowed() {
String html = "<script>doSomethingNefarious()</script>This was a dangerous script";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.EMPTY);
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo("This was a dangerous script"));
}
@Test
public void testCustom_Disabled() {
String html = "<img src=\"http://test.com/nastyimage.jpg\" />This is a bad image";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.builder()
.put("watcher.actions.email.html.sanitization.enabled", false)
.build());
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo(html));
}
@Test
public void testCustom_AllImage_Allowed() {
String html = "<img src=\"http://test.com/nastyimage.jpg\" />This is a bad image";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.builder()
.put("watcher.actions.email.html.sanitization.allow", "img:all")
.build());
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo(html));
}
@Test
public void testCustom_Tables_Disallowed() {
String html = "<table><tr><td>cell1</td><td>cell2</td></tr></table>";
HtmlSanitizer sanitizer = new HtmlSanitizer(ImmutableSettings.builder()
.put("watcher.actions.email.html.sanitization.disallow", "_tables")
.build());
String sanitizedHtml = sanitizer.sanitize(html);
assertThat(sanitizedHtml, equalTo("cell1cell2"));
}
}

View File

@ -34,10 +34,7 @@ import org.elasticsearch.watcher.actions.ActionWrapper;
import org.elasticsearch.watcher.actions.ExecutableActions;
import org.elasticsearch.watcher.actions.email.EmailAction;
import org.elasticsearch.watcher.actions.email.ExecutableEmailAction;
import org.elasticsearch.watcher.actions.email.service.Authentication;
import org.elasticsearch.watcher.actions.email.service.EmailService;
import org.elasticsearch.watcher.actions.email.service.EmailTemplate;
import org.elasticsearch.watcher.actions.email.service.Profile;
import org.elasticsearch.watcher.actions.email.service.*;
import org.elasticsearch.watcher.actions.webhook.ExecutableWebhookAction;
import org.elasticsearch.watcher.actions.webhook.WebhookAction;
import org.elasticsearch.watcher.condition.script.ExecutableScriptCondition;
@ -197,7 +194,7 @@ public final class WatcherTestUtils {
Authentication auth = new Authentication("testname", new Secret("testpassword".toCharArray()));
EmailAction action = new EmailAction(email, "testaccount", auth, Profile.STANDARD, null);
ExecutableEmailAction executale = new ExecutableEmailAction(action, logger, emailService, templateEngine);
ExecutableEmailAction executale = new ExecutableEmailAction(action, logger, emailService, templateEngine, new HtmlSanitizer(ImmutableSettings.EMPTY));
actions.add(new ActionWrapper("_email", executale));

View File

@ -26,6 +26,7 @@ import org.elasticsearch.watcher.actions.email.EmailActionFactory;
import org.elasticsearch.watcher.actions.email.ExecutableEmailAction;
import org.elasticsearch.watcher.actions.email.service.EmailService;
import org.elasticsearch.watcher.actions.email.service.EmailTemplate;
import org.elasticsearch.watcher.actions.email.service.HtmlSanitizer;
import org.elasticsearch.watcher.actions.email.service.Profile;
import org.elasticsearch.watcher.actions.index.ExecutableIndexAction;
import org.elasticsearch.watcher.actions.index.IndexAction;
@ -114,6 +115,7 @@ public class WatchTests extends ElasticsearchTestCase {
private HttpClient httpClient;
private EmailService emailService;
private TemplateEngine templateEngine;
private HtmlSanitizer htmlSanitizer;
private HttpAuthRegistry authRegistry;
private SecretService secretService;
private LicenseService licenseService;
@ -127,6 +129,7 @@ public class WatchTests extends ElasticsearchTestCase {
httpClient = mock(HttpClient.class);
emailService = mock(EmailService.class);
templateEngine = mock(TemplateEngine.class);
htmlSanitizer = mock(HtmlSanitizer.class);
secretService = mock(SecretService.class);
licenseService = mock(LicenseService.class);
authRegistry = new HttpAuthRegistry(ImmutableMap.of("basic", (HttpAuthFactory) new BasicAuthFactory(secretService)));
@ -384,7 +387,7 @@ public class WatchTests extends ElasticsearchTestCase {
if (randomBoolean()) {
ExecutableTransform transform = randomTransform();
EmailAction action = new EmailAction(EmailTemplate.builder().build(), null, null, Profile.STANDARD, randomFrom(DataAttachment.JSON, DataAttachment.YAML, null));
list.add(new ActionWrapper("_email_" + randomAsciiOfLength(8), randomThrottler(), transform, new ExecutableEmailAction(action, logger, emailService, templateEngine)));
list.add(new ActionWrapper("_email_" + randomAsciiOfLength(8), randomThrottler(), transform, new ExecutableEmailAction(action, logger, emailService, templateEngine, htmlSanitizer)));
}
if (randomBoolean()) {
IndexAction aciton = new IndexAction("_index", "_type", null);
@ -406,7 +409,7 @@ public class WatchTests extends ElasticsearchTestCase {
for (ActionWrapper action : actions) {
switch (action.action().type()) {
case EmailAction.TYPE:
parsers.put(EmailAction.TYPE, new EmailActionFactory(settings, emailService, templateEngine));
parsers.put(EmailAction.TYPE, new EmailActionFactory(settings, emailService, templateEngine, htmlSanitizer));
break;
case IndexAction.TYPE:
parsers.put(IndexAction.TYPE, new IndexActionFactory(settings, client));