blob: 9207dd8d759484e4d0366139b9ef716216fbade8 [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.ofbiz.base.util;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.ofbiz.base.html.SanitizerCustomPolicy;
import org.owasp.esapi.codecs.Codec;
import org.owasp.esapi.codecs.HTMLEntityCodec;
import org.owasp.esapi.codecs.PercentCodec;
import org.owasp.esapi.codecs.XMLEntityCodec;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
@SuppressWarnings("rawtypes")
public class UtilCodec {
private static final String MODULE = UtilCodec.class.getName();
private static final HtmlEncoder HTML_ENCODER = new HtmlEncoder();
private static final XmlEncoder XML_ENCODER = new XmlEncoder();
private static final StringEncoder STRING_ENCODER = new StringEncoder();
private static final UrlCodec URL_CODEC = new UrlCodec();
private static final List<Codec> CODECS;
// From https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Event_Handlers
private static final List<String> JS_EVENT_LIST = Arrays.asList(new String[] {"onAbort", "onActivate",
"onAfterPrint", "onAfterUpdate", "onBeforeActivate", "onBeforeCopy", "onBeforeCut", "onBeforeDeactivate",
"onBeforeEditFocus", "onBeforePaste", "onBeforePrint", "onBeforeUnload", "onBeforeUpdate", "onBegin",
"onBlur", "onBounce", "onCellChange", "onChange", "onClick", "onContextMenu", "onControlSelect", "onCopy",
"onCut", "onDataAvailable", "onDataSetChanged", "onDataSetComplete", "onDblClick", "onDeactivate", "onDrag",
"onDragEnd", "onDragLeave", "onDragEnter", "onDragOver", "onDragDrop", "onDragStart", "onDrop", "onEnd",
"onError", "onErrorUpdate", "onFilterChange", "onFinish", "onFocus", "onFocusIn", "onFocusOut",
"onHashChange", "onHelp", "onInput", "onKeyDown", "onKeyPress", "onKeyUp", "onLayoutComplete", "onLoad",
"onLoseCapture", "onMediaComplete", "onMediaError", "onMessage", "onMouseDown", "onMouseEnter",
"onMouseLeave", "onMouseMove", "onMouseOut", "onMouseOver", "onMouseUp", "onMouseWheel", "onMove",
"onMoveEnd", "onMoveStart", "onOffline", "onOnline", "onOutOfSync", "onPaste", "onPause", "onPopState",
"onProgress", "onPropertyChange", "onReadyStateChange", "onRedo", "onRepeat", "onReset", "onResize",
"onResizeEnd", "onResizeStart", "onResume", "onReverse", "onRowsEnter", "onRowExit", "onRowDelete",
"onRowInserted", "onScroll", "onSeek", "onSelect", "onSelectionChange", "onSelectStart", "onStart",
"onStop", "onStorage", "onSyncRestored", "onSubmit", "onTimeError", "onTrackChange", "onUndo", "onUnload",
"onURLFlip", "seekSegmentTime" });
static {
List<Codec> tmpCodecs = new ArrayList<>();
tmpCodecs.add(new HTMLEntityCodec());
tmpCodecs.add(new PercentCodec());
CODECS = Collections.unmodifiableList(tmpCodecs);
}
@SuppressWarnings("serial")
public static class IntrusionException extends GeneralRuntimeException {
public IntrusionException(String message) {
super(message);
}
}
public interface SimpleEncoder {
String encode(String original);
/**
* @deprecated Use {@link #sanitize(String, String)} instead
*/
@Deprecated
String sanitize(String outString); // Only really useful with HTML, else it simply calls encode() method
String sanitize(String outString, String contentTypeId); // Only really useful with HTML, else it simply calls encode() method
}
public interface SimpleDecoder {
String decode(String original);
}
public static class HtmlEncoder implements SimpleEncoder {
private static final char[] IMMUNE_HTML = {',', '.', '-', '_', ' ', ':'};
private HTMLEntityCodec htmlCodec = new HTMLEntityCodec();
@Override
public String encode(String original) {
if (original == null) {
return null;
}
return htmlCodec.encode(IMMUNE_HTML, original);
}
/**
* @deprecated Use {@link #sanitize(String, String)} instead
*/
@Override
@Deprecated
public String sanitize(String original) {
return sanitize(original, null);
}
/**
* This method will start a configurable sanitizing process. The sanitizer can
* be turns off through "sanitizer.enable=false", the default value is true. It
* is possible to configure a custom policy using the properties
* "sanitizer.permissive.policy" and "sanitizer.custom.permissive.policy.class".
* The custom policy has to implement
* {@link org.apache.ofbiz.base.html.SanitizerCustomPolicy}.
* @param original
* @param contentTypeId
* @return sanitized HTML-Code if enabled, original HTML-Code when disabled
* @see org.apache.ofbiz.base.html.CustomPermissivePolicy
*/
@Override
public String sanitize(String original, String contentTypeId) {
if (original == null) {
return null;
}
if (UtilProperties.getPropertyAsBoolean("owasp", "sanitizer.enable", true)) {
PolicyFactory sanitizer = Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.IMAGES).and(
Sanitizers.LINKS).and(Sanitizers.STYLES);
// TODO to be improved to use a (or several) contentTypeId/s when necessary.
// Below is an example with BIRT_FLEXIBLE_REPORT_POLICY
if ("FLEXIBLE_REPORT".equals(contentTypeId)) {
sanitizer = sanitizer.and(BIRT_FLEXIBLE_REPORT_POLICY);
}
// Check if custom policy should be used and if so don't use PERMISSIVE_POLICY
if ("CUSTOM".equals(UtilProperties.getPropertyValue("owasp", "sanitizer.permissive.policy"))) {
PolicyFactory policy = null;
try {
Class<?> customPolicyClass = Class.forName(UtilProperties.getPropertyValue("owasp",
"sanitizer.custom.permissive.policy.class"));
Object obj = customPolicyClass.getConstructor().newInstance();
if (SanitizerCustomPolicy.class.isAssignableFrom(customPolicyClass)) {
Method meth = customPolicyClass.getMethod("getSanitizerPolicy");
policy = (PolicyFactory) meth.invoke(obj);
}
} catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException
| InstantiationException e) {
// Just logging the error and falling back to default policy
Debug.logError(e, "Could not find custom permissive sanitizer policy. Using default instead", MODULE);
}
if (policy != null) {
sanitizer = sanitizer.and(policy);
return sanitizer.sanitize(original);
}
}
// Fallback should be the default option PERMISSIVE_POLICY
sanitizer = sanitizer.and(PERMISSIVE_POLICY);
return sanitizer.sanitize(original);
}
return original;
}
// Given as an example based on rendering cmssite as it was before using the sanitizer.
// To use the PERMISSIVE_POLICY set sanitizer.permissive.policy to true.
// Note that I was unable to render </html> and </body>. I guess because <html> and <body>
// are not sanitized in 1st place (else the sanitizer makes some damages I found)
// You might even want to adapt the PERMISSIVE_POLICY to your needs...
// Be sure to check https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet before...
// And https://github.com/OWASP/java-html-sanitizer/blob/master/docs/getting_started.md for examples.
// If you want another example:
// https://android.googlesource.com/platform/packages/apps/UnifiedEmail/+/ec0fa48/src/com/android/mail/utils/HtmlSanitizer.java
public static final PolicyFactory PERMISSIVE_POLICY = new HtmlPolicyBuilder()
.allowWithoutAttributes("html", "body")
.allowAttributes("id", "class").globally()
.allowElements("div", "center", "span", "table", "td")
.allowWithoutAttributes("html", "body", "div", "span", "table", "td")
.allowAttributes("width").onElements("table")
.toFactory();
// This is the PolicyFactory used for the Birt Report Builder usage feature. ("FLEXIBLE_REPORT" contentTypeId)
// It allows to use the OOTB Birt Report Builder example.
// You might need to enhance it for your needs (when using a new REPORT_MASTER) but normally you should not.
// See PERMISSIVE_POLICY above for documentation and examples
public static final PolicyFactory BIRT_FLEXIBLE_REPORT_POLICY = new HtmlPolicyBuilder()
.allowWithoutAttributes("html", "body")
.allowElements("form", "div", "span", "table", "tr", "td", "input", "textarea", "label", "select", "option")
.allowAttributes("id", "class", "name", "value", "onclick").globally()
.allowAttributes("width", "cellspacing").onElements("table")
.allowAttributes("type", "size", "maxlength").onElements("input")
.allowAttributes("cols", "rows").onElements("textarea")
.allowAttributes("class").onElements("td")
.allowAttributes("method").onElements("form")
.allowAttributes("accept", "action", "accept-charset", "autocomplete", "enctype", "method",
"name", "novalidate", "target").onElements("form")
.toFactory();
}
public static class XmlEncoder implements SimpleEncoder {
private static final char[] IMMUNE_XML = {',', '.', '-', '_', ' '};
private XMLEntityCodec xmlCodec = new XMLEntityCodec();
@Override
public String encode(String original) {
if (original == null) {
return null;
}
return xmlCodec.encode(IMMUNE_XML, original);
}
/**
* @deprecated Use {@link #sanitize(String, String)} instead
*/
@Override
@Deprecated
public String sanitize(String original) {
return sanitize(original, null);
}
@Override
public String sanitize(String original, String contentTypeId) {
return encode(original);
}
}
public static class UrlCodec implements SimpleEncoder, SimpleDecoder {
@Override
public String encode(String original) {
try {
return URLEncoder.encode(original, "UTF-8");
} catch (UnsupportedEncodingException ee) {
Debug.logError(ee, MODULE);
return null;
}
}
/**
* @deprecated Use {@link #sanitize(String, String)} instead
*/
@Override
@Deprecated
public String sanitize(String original) {
return sanitize(original, null);
}
@Override
public String sanitize(String original, String contentTypeId) {
return encode(original);
}
@Override
public String decode(String original) {
try {
canonicalize(original); // This is only used to show warning/s in log in case of multiple encoding/s. See OFBIZ-12014 for more
return URLDecoder.decode(original, "UTF-8");
} catch (UnsupportedEncodingException ee) {
Debug.logError(ee, MODULE);
return null;
}
}
}
public static class StringEncoder implements SimpleEncoder {
@Override
public String encode(String original) {
if (original != null) {
original = original.replace("\"", "\\\"");
}
return original;
}
/**
* @deprecated Use {@link #sanitize(String, String)} instead
*/
@Override
@Deprecated
public String sanitize(String original) {
return sanitize(original, null);
}
@Override
public String sanitize(String original, String contentTypeId) {
return encode(original);
}
}
// ================== Begin General Functions ==================
public static SimpleEncoder getEncoder(String type) {
if ("url".equals(type)) {
return URL_CODEC;
} else if ("xml".equals(type)) {
return XML_ENCODER;
} else if ("html".equals(type)) {
return HTML_ENCODER;
} else if ("string".equals(type)) {
return STRING_ENCODER;
} else {
return null;
}
}
public static SimpleDecoder getDecoder(String type) {
if ("url".equals(type)) {
return URL_CODEC;
}
return null;
}
public static String canonicalize(String value) throws IntrusionException {
return canonicalize(value, false, false);
}
public static String canonicalize(String value, boolean strict) throws IntrusionException {
return canonicalize(value, strict, strict);
}
public static String canonicalize(String input, boolean restrictMultiple, boolean restrictMixed) {
if (input == null) {
return null;
}
String working = input;
Codec codecFound = null;
int mixedCount = 1;
int foundCount = 0;
boolean clean = false;
while (!clean) {
clean = true;
// try each codec and keep track of which ones work
Iterator<Codec> i = CODECS.iterator();
while (i.hasNext()) {
Codec codec = i.next();
String old = working;
working = codec.decode(working);
if (!old.equals(working)) {
if (codecFound != null && codecFound != codec) {
mixedCount++;
}
codecFound = codec;
if (clean) {
foundCount++;
}
clean = false;
}
}
}
// do strict tests and handle if any mixed, multiple, nested encoding were found
if (foundCount >= 2 && mixedCount > 1) {
if (restrictMultiple || restrictMixed) {
throw new IntrusionException("Input validation failure");
}
Debug.logWarning("Multiple (" + foundCount + "x) and mixed encoding (" + mixedCount + "x) detected in " + input, MODULE);
} else if (foundCount >= 2) {
if (restrictMultiple) {
throw new IntrusionException("Input validation failure");
}
Debug.logWarning("Multiple (" + foundCount + "x) encoding detected in " + input, MODULE);
} else if (mixedCount > 1) {
if (restrictMixed) {
throw new IntrusionException("Input validation failure");
}
Debug.logWarning("Mixed encoding (" + mixedCount + "x) detected in " + input, MODULE);
}
return working;
}
/**
* Uses a black-list approach for necessary characters for HTML.
* Does not allow various characters (after canonicalization), including
* "&lt;", "&gt;", "&amp;" and "%" (if not followed by a space).
* Also does not allow js events as in OFBIZ-10054
* @param valueName field name checked
* @param value value checked
* @param errorMessageList an empty list passed by and modified in case of issues
* @param locale
*/
public static String checkStringForHtmlStrictNone(String valueName, String value, List<String> errorMessageList,
Locale locale) {
if (UtilValidate.isEmpty(value)) {
return value;
}
// canonicalize, strict (error on double-encoding)
try {
value = canonicalize(value, true);
} catch (IntrusionException e) {
// NOTE: using different log and user targeted error messages to allow the end-user message to be less technical
Debug.logError("Canonicalization (format consistency, character escaping that is mixed or double, etc) "
+ "error for attribute named [" + valueName + "], String [" + value + "]: " + e.toString(), MODULE);
String issueMsg = null;
if (locale.equals(new Locale("test"))) { // labels are not available in testClasses Gradle task
issueMsg = "In field [" + valueName + "] found character escaping (mixed or double) "
+ "that is not allowed or other format consistency error: ";
} else {
issueMsg = UtilProperties.getMessage("SecurityUiLabels", "PolicyNoneMixedOrDouble",
UtilMisc.toMap("valueName", valueName), locale);
}
errorMessageList.add(issueMsg + e.toString());
}
// check for "<", ">"
if (value.indexOf("<") >= 0 || value.indexOf(">") >= 0) {
String issueMsg = null;
if (locale.equals(new Locale("test"))) {
issueMsg = "In field [" + valueName + "] less-than (<) and greater-than (>) symbols are not allowed.";
} else {
issueMsg = UtilProperties.getMessage("SecurityUiLabels", "PolicyNoneLess-thanGreater-than",
UtilMisc.toMap("valueName", valueName), locale);
}
errorMessageList.add(issueMsg);
}
// check for js events
String onEvent = "on" + StringUtils.substringBetween(value, " on", "=");
if (JS_EVENT_LIST.stream().anyMatch(str -> StringUtils.containsIgnoreCase(str, onEvent))
|| value.contains("seekSegmentTime")) {
String issueMsg = null;
if (locale.equals(new Locale("test"))) {
issueMsg = "In field [" + valueName + "] Javascript events are not allowed.";
} else {
issueMsg = UtilProperties.getMessage("SecurityUiLabels", "PolicyNoneJsEvents",
UtilMisc.toMap("valueName", valueName), locale);
}
errorMessageList.add(issueMsg);
}
// TODO: anything else to check for that can be used to get HTML or JavaScript going without these characters?
//
// Another would be https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#US-ASCII_encoding
// But all our Tomcat connectors use UTF-8
// We don't care about Flash now rather deprecated
// AFAIK all others need less-than (<) and greater-than (>) symbols
return value;
}
/**
* This method check if the input is safe HTML.
* It is possible to configure a safe policy using the properties
* "sanitizer.safe.policy" and "sanitizer.custom.safe.policy.class".
* The safe policy has to implement
* {@link org.apache.ofbiz.base.html.SanitizerCustomPolicy}.
* @param valueName field name checked
* @param value value checked
* @param errorMessageList an empty list passed by and modified in case of issues
* @param locale
*/
public static String checkStringForHtmlSafe(String valueName, String value, List<String> errorMessageList,
Locale locale, boolean enableSanitizer) {
if (!enableSanitizer) {
return value;
}
PolicyFactory policy = null;
try {
Class<?> customPolicyClass = null;
if (locale.equals(new Locale("test"))) {
customPolicyClass = Class.forName("org.apache.ofbiz.base.html.CustomSafePolicy");
} else {
customPolicyClass = Class.forName(UtilProperties.getPropertyValue("owasp", "sanitizer.custom.safe.policy.class"));
}
Object obj = customPolicyClass.getConstructor().newInstance();
if (SanitizerCustomPolicy.class.isAssignableFrom(customPolicyClass)) {
Method meth = customPolicyClass.getMethod("getSanitizerPolicy");
policy = (PolicyFactory) meth.invoke(obj);
}
} catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException
| InstantiationException e) {
Debug.logError(e, "Could not find custom safe sanitizer policy. Using default instead."
+ "Beware: the result is not rightly checked!", MODULE);
}
if (value != null) {
String filtered = policy.sanitize(value);
if (filtered != null && !value.equals(StringEscapeUtils.unescapeEcmaScript(StringEscapeUtils.unescapeHtml4(filtered)))) {
String issueMsg = null;
if (locale.equals(new Locale("test"))) {
issueMsg = "In field [" + valueName + "] by our input policy, your input has not been accepted "
+ "for security reason. Please check and modify accordingly, thanks.";
} else {
issueMsg = UtilProperties.getMessage("SecurityUiLabels", "PolicySafe",
UtilMisc.toMap("valueName", valueName), locale);
}
errorMessageList.add(issueMsg);
}
}
return value;
}
/**
* A simple Map wrapper class that will do HTML encoding.
* To be used for passing a Map to something that will expand Strings with it as a context, etc.
*/
public static class HtmlEncodingMapWrapper<K> implements Map<K, Object> {
public static <K> HtmlEncodingMapWrapper<K> getHtmlEncodingMapWrapper(Map<K, Object> mapToWrap, SimpleEncoder encoder) {
if (mapToWrap == null) {
return null;
}
HtmlEncodingMapWrapper<K> mapWrapper = new HtmlEncodingMapWrapper<>();
mapWrapper.setup(mapToWrap, encoder);
return mapWrapper;
}
private Map<K, Object> internalMap = null;
private SimpleEncoder encoder = null;
protected HtmlEncodingMapWrapper() { }
/**
* Sets .
* @param mapToWrap the map to wrap
* @param encoder the encoder
*/
public void setup(Map<K, Object> mapToWrap, SimpleEncoder encoder) {
this.internalMap = mapToWrap;
this.encoder = encoder;
}
/**
* Reset.
*/
public void reset() {
this.internalMap = null;
this.encoder = null;
}
@Override
public int size() {
return this.internalMap.size();
}
@Override
public boolean isEmpty() {
return this.internalMap.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return this.internalMap.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return this.internalMap.containsValue(value);
}
@Override
public Object get(Object key) {
Object theObject = this.internalMap.get(key);
if (theObject instanceof String) {
if (this.encoder != null) {
return encoder.encode((String) theObject);
}
return UtilCodec.getEncoder("html").encode((String) theObject);
} else if (theObject instanceof Map<?, ?>) {
return HtmlEncodingMapWrapper.getHtmlEncodingMapWrapper(UtilGenerics.cast(theObject), this.encoder);
}
return theObject;
}
@Override
public Object put(K key, Object value) {
return this.internalMap.put(key, value);
}
@Override
public Object remove(Object key) {
return this.internalMap.remove(key);
}
@Override
public void putAll(Map<? extends K, ? extends Object> arg0) {
this.internalMap.putAll(arg0);
}
@Override
public void clear() {
this.internalMap.clear();
}
@Override
public Set<K> keySet() {
return this.internalMap.keySet();
}
@Override
public Collection<Object> values() {
return this.internalMap.values();
}
@Override
public Set<Map.Entry<K, Object>> entrySet() {
return this.internalMap.entrySet();
}
@Override
public String toString() {
return this.internalMap.toString();
}
}
}