KEYCLOAK-7479: Sanitize
This commit is contained in:
parent
f57cc3a9c0
commit
e7e15652cf
30 changed files with 383 additions and 23 deletions
|
@ -72,6 +72,16 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||||
|
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>*</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.thoughtworks.xstream</groupId>
|
<groupId>com.thoughtworks.xstream</groupId>
|
||||||
<artifactId>xstream</artifactId>
|
<artifactId>xstream</artifactId>
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<module xmlns="urn:jboss:module:1.3" name="com.googlecode.owasp-java-html-sanitizer">
|
||||||
|
<resources>
|
||||||
|
<artifact name="${com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer}"/>
|
||||||
|
</resources>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<module name="com.google.guava"/>
|
||||||
|
</dependencies>
|
||||||
|
</module>
|
|
@ -44,6 +44,8 @@
|
||||||
<module name="org.keycloak.keycloak-authz-policy-common" services="import"/>
|
<module name="org.keycloak.keycloak-authz-policy-common" services="import"/>
|
||||||
<module name="org.keycloak.keycloak-authz-policy-drools" services="import"/>
|
<module name="org.keycloak.keycloak-authz-policy-drools" services="import"/>
|
||||||
|
|
||||||
|
<module name="com.googlecode.owasp-java-html-sanitizer"/>
|
||||||
|
<module name="com.google.guava"/>
|
||||||
<module name="org.freemarker"/>
|
<module name="org.freemarker"/>
|
||||||
<module name="javax.ws.rs.api"/>
|
<module name="javax.ws.rs.api"/>
|
||||||
<module name="javax.mail.api"/>
|
<module name="javax.mail.api"/>
|
||||||
|
|
6
pom.xml
6
pom.xml
|
@ -77,6 +77,7 @@
|
||||||
<jboss.spec.javax.xml.bind.jboss-jaxb-api_2.2_spec.version>1.0.4.Final</jboss.spec.javax.xml.bind.jboss-jaxb-api_2.2_spec.version>
|
<jboss.spec.javax.xml.bind.jboss-jaxb-api_2.2_spec.version>1.0.4.Final</jboss.spec.javax.xml.bind.jboss-jaxb-api_2.2_spec.version>
|
||||||
<log4j.version>1.2.17</log4j.version>
|
<log4j.version>1.2.17</log4j.version>
|
||||||
<resteasy.version>3.0.26.Final</resteasy.version>
|
<resteasy.version>3.0.26.Final</resteasy.version>
|
||||||
|
<owasp.html.sanitizer.version>20180219.1</owasp.html.sanitizer.version>
|
||||||
<slf4j.version>1.7.22</slf4j.version>
|
<slf4j.version>1.7.22</slf4j.version>
|
||||||
<sun.istack.version>2.21</sun.istack.version>
|
<sun.istack.version>2.21</sun.istack.version>
|
||||||
<sun.jaxb.version>2.2.11</sun.jaxb.version>
|
<sun.jaxb.version>2.2.11</sun.jaxb.version>
|
||||||
|
@ -370,6 +371,11 @@
|
||||||
<version>${log4j.version}</version>
|
<version>${log4j.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||||
|
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||||
|
<version>${owasp.html.sanitizer.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"dependencyExclusion.io.undertow:*@*": "1.4.18.SP2-redhat-1",
|
"dependencyExclusion.io.undertow:*@*": "1.4.18.SP2-redhat-1",
|
||||||
"dependencyExclusion.org.wildfly.security:*@*": "1.1.8.Final-redhat-1",
|
"dependencyExclusion.org.wildfly.security:*@*": "1.1.8.Final-redhat-1",
|
||||||
"dependencyExclusion.org.freemarker:freemarker@*": "2.3.26.incubating-redhat-1",
|
"dependencyExclusion.org.freemarker:freemarker@*": "2.3.26.incubating-redhat-1",
|
||||||
|
"dependencyExclusion.com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer@*": "20180219.1.0.redhat-1",
|
||||||
"dependencyExclusion.org.liquibase:liquibase-core@*": "$COMMONCFG_LIQUIBASE_3_4_1",
|
"dependencyExclusion.org.liquibase:liquibase-core@*": "$COMMONCFG_LIQUIBASE_3_4_1",
|
||||||
"dependencyExclusion.org.twitter4j:twitter4j-core@*": "$COMMONCFG_TWITTER4J_4_0_4",
|
"dependencyExclusion.org.twitter4j:twitter4j-core@*": "$COMMONCFG_TWITTER4J_4_0_4",
|
||||||
"dependencyExclusion.com.google.zxing:core@*": "$COMMONCFG_ZXING_3_2_1",
|
"dependencyExclusion.com.google.zxing:core@*": "$COMMONCFG_ZXING_3_2_1",
|
||||||
|
|
|
@ -136,6 +136,10 @@
|
||||||
<groupId>org.jboss.resteasy</groupId>
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
<artifactId>resteasy-multipart-provider</artifactId>
|
<artifactId>resteasy-multipart-provider</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||||
|
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-core</artifactId>
|
<artifactId>jackson-core</artifactId>
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +36,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
public class FreeMarkerUtil {
|
public class FreeMarkerUtil {
|
||||||
|
|
||||||
private ConcurrentHashMap<String, Template> cache;
|
private ConcurrentHashMap<String, Template> cache;
|
||||||
|
private final KeycloakSanitizerMethod kcSanitizeMethod = new KeycloakSanitizerMethod();
|
||||||
|
|
||||||
public FreeMarkerUtil() {
|
public FreeMarkerUtil() {
|
||||||
if (Config.scope("theme").getBoolean("cacheTemplates", true)) {
|
if (Config.scope("theme").getBoolean("cacheTemplates", true)) {
|
||||||
|
@ -43,6 +45,10 @@ public class FreeMarkerUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String processTemplate(Object data, String templateName, Theme theme) throws FreeMarkerException {
|
public String processTemplate(Object data, String templateName, Theme theme) throws FreeMarkerException {
|
||||||
|
if (data instanceof Map) {
|
||||||
|
((Map)data).put("kcSanitize", kcSanitizeMethod);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Template template;
|
Template template;
|
||||||
cache = null;
|
cache = null;
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.theme;
|
||||||
|
|
||||||
|
import freemarker.template.TemplateMethodModelEx;
|
||||||
|
import freemarker.template.TemplateModelException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.owasp.html.PolicyFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows sanitizing of html that uses Freemarker ?no_esc. This way, html
|
||||||
|
* can be allowed but it is still cleaned up for safety. Tags and attributes
|
||||||
|
* deemed unsafe will be stripped out.
|
||||||
|
*/
|
||||||
|
public class KeycloakSanitizerMethod implements TemplateMethodModelEx {
|
||||||
|
|
||||||
|
private static final PolicyFactory KEYCLOAK_POLICY = KeycloakSanitizerPolicy.POLICY_DEFINITION;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object exec(List list) throws TemplateModelException {
|
||||||
|
if ((list.isEmpty()) || (list.get(0) == null)) {
|
||||||
|
throw new NullPointerException("Can not escape null value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String html = list.get(0).toString();
|
||||||
|
String sanitized = KEYCLOAK_POLICY.sanitize(html);
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.theme;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.owasp.html.HtmlPolicyBuilder;
|
||||||
|
import org.owasp.html.PolicyFactory;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on the EbayPolicyExample in owasp java-html-sanitizer.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class KeycloakSanitizerPolicy {
|
||||||
|
|
||||||
|
// Some common regular expression definitions.
|
||||||
|
|
||||||
|
// The 16 colors defined by the HTML Spec (also used by the CSS Spec)
|
||||||
|
private static final Pattern COLOR_NAME = Pattern.compile(
|
||||||
|
"(?:aqua|black|blue|fuchsia|gray|grey|green|lime|maroon|navy|olive|purple"
|
||||||
|
+ "|red|silver|teal|white|yellow)");
|
||||||
|
|
||||||
|
// HTML/CSS Spec allows 3 or 6 digit hex to specify color
|
||||||
|
private static final Pattern COLOR_CODE = Pattern.compile(
|
||||||
|
"(?:#(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?))");
|
||||||
|
|
||||||
|
private static final Pattern NUMBER_OR_PERCENT = Pattern.compile(
|
||||||
|
"[0-9]+%?");
|
||||||
|
private static final Pattern PARAGRAPH = Pattern.compile(
|
||||||
|
"(?:[\\p{L}\\p{N},'\\.\\s\\-_\\(\\)]|&[0-9]{2};)*");
|
||||||
|
private static final Pattern HTML_ID = Pattern.compile(
|
||||||
|
"[a-zA-Z0-9\\:\\-_\\.]+");
|
||||||
|
// force non-empty with a '+' at the end instead of '*'
|
||||||
|
private static final Pattern HTML_TITLE = Pattern.compile(
|
||||||
|
"[\\p{L}\\p{N}\\s\\-_',:\\[\\]!\\./\\\\\\(\\)&]*");
|
||||||
|
private static final Pattern HTML_CLASS = Pattern.compile(
|
||||||
|
"[a-zA-Z0-9\\s,\\-_]+");
|
||||||
|
|
||||||
|
private static final Pattern ONSITE_URL = Pattern.compile(
|
||||||
|
"(?:[\\p{L}\\p{N}\\\\\\.\\#@\\$%\\+&;\\-_~,\\?=/!]+|\\#(\\w)+)");
|
||||||
|
private static final Pattern OFFSITE_URL = Pattern.compile(
|
||||||
|
"\\s*(?:(?:ht|f)tps?://|mailto:)[\\p{L}\\p{N}]"
|
||||||
|
+ "[\\p{L}\\p{N}\\p{Zs}\\.\\#@\\$%\\+&;:\\-_~,\\?=/!\\(\\)]*+\\s*");
|
||||||
|
|
||||||
|
private static final Pattern NUMBER = Pattern.compile(
|
||||||
|
"[+-]?(?:(?:[0-9]+(?:\\.[0-9]*)?)|\\.[0-9]+)");
|
||||||
|
|
||||||
|
private static final Pattern NAME = Pattern.compile("[a-zA-Z0-9\\-_\\$]+");
|
||||||
|
|
||||||
|
private static final Pattern ALIGN = Pattern.compile(
|
||||||
|
"(?i)center|left|right|justify|char");
|
||||||
|
|
||||||
|
private static final Pattern VALIGN = Pattern.compile(
|
||||||
|
"(?i)baseline|bottom|middle|top");
|
||||||
|
|
||||||
|
private static final Predicate<String> COLOR_NAME_OR_COLOR_CODE
|
||||||
|
= matchesEither(COLOR_NAME, COLOR_CODE);
|
||||||
|
|
||||||
|
private static final Predicate<String> ONSITE_OR_OFFSITE_URL
|
||||||
|
= matchesEither(ONSITE_URL, OFFSITE_URL);
|
||||||
|
|
||||||
|
private static final Pattern HISTORY_BACK = Pattern.compile(
|
||||||
|
"(?:javascript:)?\\Qhistory.go(-1)\\E");
|
||||||
|
|
||||||
|
private static final Pattern ONE_CHAR = Pattern.compile(
|
||||||
|
".?", Pattern.DOTALL);
|
||||||
|
|
||||||
|
|
||||||
|
public static final PolicyFactory POLICY_DEFINITION = new HtmlPolicyBuilder()
|
||||||
|
.allowWithoutAttributes("span") // this is added to ebay example to allow span without attributes
|
||||||
|
.allowAttributes("id").matching(HTML_ID).globally()
|
||||||
|
.allowAttributes("class").matching(HTML_CLASS).globally()
|
||||||
|
.allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}"))
|
||||||
|
.globally()
|
||||||
|
.allowAttributes("title").matching(HTML_TITLE).globally()
|
||||||
|
.allowStyling()
|
||||||
|
.allowAttributes("align").matching(ALIGN).onElements("p")
|
||||||
|
.allowAttributes("for").matching(HTML_ID).onElements("label")
|
||||||
|
.allowAttributes("color").matching(COLOR_NAME_OR_COLOR_CODE)
|
||||||
|
.onElements("font")
|
||||||
|
.allowAttributes("face")
|
||||||
|
.matching(Pattern.compile("[\\w;, \\-]+"))
|
||||||
|
.onElements("font")
|
||||||
|
.allowAttributes("size").matching(NUMBER).onElements("font")
|
||||||
|
.allowAttributes("href").matching(ONSITE_OR_OFFSITE_URL)
|
||||||
|
.onElements("a")
|
||||||
|
.allowStandardUrlProtocols()
|
||||||
|
.allowAttributes("nohref").onElements("a")
|
||||||
|
.allowAttributes("name").matching(NAME).onElements("a")
|
||||||
|
.allowAttributes(
|
||||||
|
"onfocus", "onblur", "onclick", "onmousedown", "onmouseup")
|
||||||
|
.matching(HISTORY_BACK).onElements("a")
|
||||||
|
.requireRelNofollowOnLinks()
|
||||||
|
.allowAttributes("src").matching(ONSITE_OR_OFFSITE_URL)
|
||||||
|
.onElements("img")
|
||||||
|
.allowAttributes("name").matching(NAME)
|
||||||
|
.onElements("img")
|
||||||
|
.allowAttributes("alt").matching(PARAGRAPH)
|
||||||
|
.onElements("img")
|
||||||
|
.allowAttributes("border", "hspace", "vspace").matching(NUMBER)
|
||||||
|
.onElements("img")
|
||||||
|
.allowAttributes("border", "cellpadding", "cellspacing")
|
||||||
|
.matching(NUMBER).onElements("table")
|
||||||
|
.allowAttributes("bgcolor").matching(COLOR_NAME_OR_COLOR_CODE)
|
||||||
|
.onElements("table")
|
||||||
|
.allowAttributes("background").matching(ONSITE_URL)
|
||||||
|
.onElements("table")
|
||||||
|
.allowAttributes("align").matching(ALIGN)
|
||||||
|
.onElements("table")
|
||||||
|
.allowAttributes("noresize").matching(Pattern.compile("(?i)noresize"))
|
||||||
|
.onElements("table")
|
||||||
|
.allowAttributes("background").matching(ONSITE_URL)
|
||||||
|
.onElements("td", "th", "tr")
|
||||||
|
.allowAttributes("bgcolor").matching(COLOR_NAME_OR_COLOR_CODE)
|
||||||
|
.onElements("td", "th")
|
||||||
|
.allowAttributes("abbr").matching(PARAGRAPH)
|
||||||
|
.onElements("td", "th")
|
||||||
|
.allowAttributes("axis", "headers").matching(NAME)
|
||||||
|
.onElements("td", "th")
|
||||||
|
.allowAttributes("scope")
|
||||||
|
.matching(Pattern.compile("(?i)(?:row|col)(?:group)?"))
|
||||||
|
.onElements("td", "th")
|
||||||
|
.allowAttributes("nowrap")
|
||||||
|
.onElements("td", "th")
|
||||||
|
.allowAttributes("height", "width").matching(NUMBER_OR_PERCENT)
|
||||||
|
.onElements("table", "td", "th", "tr", "img")
|
||||||
|
.allowAttributes("align").matching(ALIGN)
|
||||||
|
.onElements("thead", "tbody", "tfoot", "img",
|
||||||
|
"td", "th", "tr", "colgroup", "col")
|
||||||
|
.allowAttributes("valign").matching(VALIGN)
|
||||||
|
.onElements("thead", "tbody", "tfoot",
|
||||||
|
"td", "th", "tr", "colgroup", "col")
|
||||||
|
.allowAttributes("charoff").matching(NUMBER_OR_PERCENT)
|
||||||
|
.onElements("td", "th", "tr", "colgroup", "col",
|
||||||
|
"thead", "tbody", "tfoot")
|
||||||
|
.allowAttributes("char").matching(ONE_CHAR)
|
||||||
|
.onElements("td", "th", "tr", "colgroup", "col",
|
||||||
|
"thead", "tbody", "tfoot")
|
||||||
|
.allowAttributes("colspan", "rowspan").matching(NUMBER)
|
||||||
|
.onElements("td", "th")
|
||||||
|
.allowAttributes("span", "width").matching(NUMBER_OR_PERCENT)
|
||||||
|
.onElements("colgroup", "col")
|
||||||
|
.allowElements(
|
||||||
|
"a", "label", "noscript", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
|
"p", "i", "b", "u", "strong", "em", "small", "big", "pre", "code",
|
||||||
|
"cite", "samp", "sub", "sup", "strike", "center", "blockquote",
|
||||||
|
"hr", "br", "col", "font", "map", "span", "div", "img",
|
||||||
|
"ul", "ol", "li", "dd", "dt", "dl", "tbody", "thead", "tfoot",
|
||||||
|
"table", "td", "th", "tr", "colgroup", "fieldset", "legend")
|
||||||
|
.toFactory();
|
||||||
|
|
||||||
|
private static Predicate<String> matchesEither(
|
||||||
|
final Pattern a, final Pattern b) {
|
||||||
|
return new Predicate<String>() {
|
||||||
|
public boolean apply(String s) {
|
||||||
|
return a.matcher(s).matches()|| b.matcher(s).matches();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.theme;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the KeycloakEscape utility.
|
||||||
|
*
|
||||||
|
* @author Stan Silvert
|
||||||
|
*/
|
||||||
|
public class KeycloakSanitizerTest {
|
||||||
|
private KeycloakSanitizerMethod kcEscape = new KeycloakSanitizerMethod();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEscapes() throws Exception {
|
||||||
|
List<String> html = new ArrayList();
|
||||||
|
|
||||||
|
html.add("<div class=\"kc-logo-text\"><script>alert('foo');</script><span>Keycloak</span></div>");
|
||||||
|
String expectedResult = "<div class=\"kc-logo-text\"><span>Keycloak</span></div>";
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
|
||||||
|
html.set(0, "<h1>Foo</h1>");
|
||||||
|
expectedResult = "<h1>Foo</h1>";
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
|
||||||
|
html.set(0, "<div class=\"kc-logo-text\"><span>Keycloak</span></div><svg onload=alert(document.cookie);>");
|
||||||
|
expectedResult = "<div class=\"kc-logo-text\"><span>Keycloak</span></div>";
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
|
||||||
|
html.set(0, null);
|
||||||
|
expectedResult = null;
|
||||||
|
try {
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
fail("Expected NPE");
|
||||||
|
} catch (NullPointerException npe) {}
|
||||||
|
|
||||||
|
html.set(0, "");
|
||||||
|
expectedResult = "";
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertResult(String expectedResult, List<String> html) throws Exception {
|
||||||
|
String result = kcEscape.exec(html).toString();
|
||||||
|
assertEquals(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -47,7 +47,13 @@ public class MailUtils {
|
||||||
|
|
||||||
public static String getPasswordResetEmailLink(EmailBody body) throws IOException, MessagingException {
|
public static String getPasswordResetEmailLink(EmailBody body) throws IOException, MessagingException {
|
||||||
final String textChangePwdUrl = getLink(body.getText());
|
final String textChangePwdUrl = getLink(body.getText());
|
||||||
final String htmlChangePwdUrl = getLink(body.getHtml());
|
String htmlChangePwdUrl = getLink(body.getHtml());
|
||||||
|
|
||||||
|
// undo changes that may have been made by html sanitizer
|
||||||
|
htmlChangePwdUrl = htmlChangePwdUrl.replace("=", "=");
|
||||||
|
htmlChangePwdUrl = htmlChangePwdUrl.replace("..", ".");
|
||||||
|
htmlChangePwdUrl = htmlChangePwdUrl.replace("&", "&");
|
||||||
|
|
||||||
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
|
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
|
||||||
|
|
||||||
return htmlChangePwdUrl;
|
return htmlChangePwdUrl;
|
||||||
|
|
|
@ -357,7 +357,13 @@ public abstract class AbstractIdentityProviderTest {
|
||||||
|
|
||||||
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
||||||
|
|
||||||
final String htmlChangePwdUrl = MailUtil.getLink(htmlBody);
|
String htmlChangePwdUrl = MailUtil.getLink(htmlBody);
|
||||||
|
|
||||||
|
// undo changes that may have been made by html sanitizer
|
||||||
|
htmlChangePwdUrl = htmlChangePwdUrl.replace("=", "=");
|
||||||
|
htmlChangePwdUrl = htmlChangePwdUrl.replace("..", ".");
|
||||||
|
htmlChangePwdUrl = htmlChangePwdUrl.replace("&", "&");
|
||||||
|
|
||||||
assertEquals(htmlChangePwdUrl, textVerificationUrl);
|
assertEquals(htmlChangePwdUrl, textVerificationUrl);
|
||||||
|
|
||||||
return htmlChangePwdUrl;
|
return htmlChangePwdUrl;
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||||
<div class="">
|
<div class="">
|
||||||
<#if url.referrerURI??><a href="${url.referrerURI}">${msg("backToApplication")?no_esc}/a></#if>
|
<#if url.referrerURI??><a href="${url.referrerURI}">${kcSanitize(msg("backToApplication")?no_esc)}</a></#if>
|
||||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<div class="alert alert-${message.type}">
|
<div class="alert alert-${message.type}">
|
||||||
<#if message.type=='success' ><span class="pficon pficon-ok"></span></#if>
|
<#if message.type=='success' ><span class="pficon pficon-ok"></span></#if>
|
||||||
<#if message.type=='error' ><span class="pficon pficon-error-octagon"></span><span class="pficon pficon-error-exclamation"></span></#if>
|
<#if message.type=='error' ><span class="pficon pficon-error-octagon"></span><span class="pficon pficon-error-exclamation"></span></#if>
|
||||||
${message.summary?no_esc}
|
${kcSanitize(message.summary)?no_esc}
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("emailTestBodyHtml",realmName)?no_esc}
|
${kcSanitize(msg("emailTestBodyHtml",realmName))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("emailVerificationBodyCodeHtml",code)?no_esc}
|
${kcSanitize(msg("emailVerificationBodyCodeHtml",code))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))?no_esc}
|
${kcSanitize(msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("eventLoginErrorBodyHtml",event.date,event.ipAddress)?no_esc}
|
${kcSanitize(msg("eventLoginErrorBodyHtml",event.date,event.ipAddress))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("eventRemoveTotpBodyHtml",event.date, event.ipAddress)?no_esc}
|
${kcSanitize(msg("eventRemoveTotpBodyHtml",event.date, event.ipAddress))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("eventUpdatePasswordBodyHtml",event.date, event.ipAddress)?no_esc}
|
${kcSanitize(msg("eventUpdatePasswordBodyHtml",event.date, event.ipAddress))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("eventUpdateTotpBodyHtml",event.date, event.ipAddress)?no_esc}
|
${kcSanitize(msg("eventUpdateTotpBodyHtml",event.date, event.ipAddress))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration))?no_esc}
|
${kcSanitize(msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration)))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration))?no_esc}
|
${kcSanitize(msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration)))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("passwordResetBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))?no_esc}
|
${kcSanitize(msg("passwordResetBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -6,7 +6,7 @@
|
||||||
<div id="kc-error-message">
|
<div id="kc-error-message">
|
||||||
<p class="instruction">${message.summary}</p>
|
<p class="instruction">${message.summary}</p>
|
||||||
<#if client?? && client.baseUrl?has_content>
|
<#if client?? && client.baseUrl?has_content>
|
||||||
<p><a id="backToApplication" href="${client.baseUrl}">${msg("backToApplication")?no_esc}</a></p>
|
<p><a id="backToApplication" href="${client.baseUrl}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
|
||||||
</#if>
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
|
@ -12,11 +12,11 @@
|
||||||
<#if skipLink??>
|
<#if skipLink??>
|
||||||
<#else>
|
<#else>
|
||||||
<#if pageRedirectUri??>
|
<#if pageRedirectUri??>
|
||||||
<p><a href="${pageRedirectUri}">${msg("backToApplication")?no_esc}</a></p>
|
<p><a href="${pageRedirectUri}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
|
||||||
<#elseif actionUri??>
|
<#elseif actionUri??>
|
||||||
<p><a href="${actionUri}">${msg("proceedWithAction")?no_esc}</a></p>
|
<p><a href="${actionUri}">${kcSanitize(msg("proceedWithAction"))?no_esc}</a></p>
|
||||||
<#elseif client.baseUrl??>
|
<#elseif client.baseUrl??>
|
||||||
<p><a href="${client.baseUrl}">${msg("backToApplication")?no_esc}</a></p>
|
<p><a href="${client.baseUrl}">${kcSanitize(msg("backToApplication"))?no_esc}</a></p>
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
||||||
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||||
<div class="${properties.kcFormOptionsWrapperClass!}">
|
<div class="${properties.kcFormOptionsWrapperClass!}">
|
||||||
<span><a href="${url.loginUrl}">${msg("backToLogin")?no_esc}</a></span>
|
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||||
<div class="${properties.kcFormOptionsWrapperClass!}">
|
<div class="${properties.kcFormOptionsWrapperClass!}">
|
||||||
<span><a href="${url.loginUrl}">${msg("backToLogin")?no_esc}</a></span>
|
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<body class="${properties.kcBodyClass!}">
|
<body class="${properties.kcBodyClass!}">
|
||||||
<div class="${properties.kcLoginClass!}">
|
<div class="${properties.kcLoginClass!}">
|
||||||
<div id="kc-header" class="${properties.kcHeaderClass!}">
|
<div id="kc-header" class="${properties.kcHeaderClass!}">
|
||||||
<div id="kc-header-wrapper" class="${properties.kcHeaderWrapperClass!}">${msg("loginTitleHtml",(realm.displayNameHtml!''))?no_esc}</div>
|
<div id="kc-header-wrapper" class="${properties.kcHeaderWrapperClass!}">${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="${properties.kcFormCardClass!} <#if displayWide>${properties.kcFormCardAccountClass!}</#if>">
|
<div class="${properties.kcFormCardClass!} <#if displayWide>${properties.kcFormCardAccountClass!}</#if>">
|
||||||
<header class="${properties.kcFormHeaderClass!}">
|
<header class="${properties.kcFormHeaderClass!}">
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
<#if message.type = 'warning'><span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
|
<#if message.type = 'warning'><span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
|
||||||
<#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
|
<#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
|
||||||
<#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
|
<#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
|
||||||
<span class="kc-feedback-text">${message.summary?no_esc}</span>
|
<span class="kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
${msg("termsTitle")}
|
${msg("termsTitle")}
|
||||||
<#elseif section = "form">
|
<#elseif section = "form">
|
||||||
<div id="kc-terms-text">
|
<div id="kc-terms-text">
|
||||||
${msg("termsText")?no_esc}
|
${kcSanitize(msg("termsText"))?no_esc}
|
||||||
</div>
|
</div>
|
||||||
<form class="form-actions" action="${url.loginAction}" method="POST">
|
<form class="form-actions" action="${url.loginAction}" method="POST">
|
||||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doAccept")}"/>
|
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doAccept")}"/>
|
||||||
|
|
Loading…
Reference in a new issue