blob: 5a63bf737ade0f866dbfd39fc8e557f06361e12a [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.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
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;
public class UtilCodec {
private static final String module = UtilCodec.class.getName();
private static final HtmlEncoder htmlEncoder = new HtmlEncoder();
private static final XmlEncoder xmlEncoder = new XmlEncoder();
private static final StringEncoder stringEncoder = new StringEncoder();
private static final UrlCodec urlCodec = new UrlCodec();
private static final List<Codec> codecs;
static {
List<Codec> tmpCodecs = new ArrayList<Codec>();
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 static interface SimpleEncoder {
public String encode(String original);
/**
* @deprecated Use {@link #sanitize(String,String)} instead
*/
public String sanitize(String outString); // Only really useful with HTML, else it simply calls encode() method
public String sanitize(String outString, String contentTypeId); // Only really useful with HTML, else it simply calls encode() method
}
public static interface SimpleDecoder {
public String decode(String original);
}
public static class HtmlEncoder implements SimpleEncoder {
private static final char[] IMMUNE_HTML = {',', '.', '-', '_', ' '};
private HTMLEntityCodec htmlCodec = new HTMLEntityCodec();
public String encode(String original) {
if (original == null) {
return null;
}
return htmlCodec.encode(IMMUNE_HTML, original);
}
/**
* @deprecated Use {@link #sanitize(String,String)} instead
*/
public String sanitize(String original) {
return sanitize(original, null);
}
public String sanitize(String original, String contentTypeId) {
if (original == null) {
return null;
}
PolicyFactory sanitizer = Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.IMAGES).and(Sanitizers.LINKS).and(Sanitizers.STYLES);
if (UtilProperties.getPropertyAsBoolean("owasp", "sanitizer.permissive.policy", false)) {// TODO to be improved to use a (or several) contentTypeId/s if possible
sanitizer = sanitizer.and(PERMISSIVE_POLICY);
}
if ("REPORT_MASTER".equals(contentTypeId)) {
sanitizer = sanitizer.and(BIRT_REPORT_BUILDER_GENERATION_POLICY);
}
if ("REPORT".equals(contentTypeId)) {
sanitizer = sanitizer.and(BIRT_REPORT_BUILDER_USAGE_POLICY);
}
return sanitizer.sanitize(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...
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 generation feature ("REPORT_MASTER" contentTypeId)
// It allows to create the OOTB Birt Report Builder example.
// You might need to enhance it for your needs but normally you should not
// In any case be sure to check https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet before changing things here...
public static final PolicyFactory BIRT_REPORT_BUILDER_GENERATION_POLICY = new HtmlPolicyBuilder()
.allowWithoutAttributes("html", "body")
.allowElements("div", "span", "table", "tr", "td")
.allowElements("form", "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")
.toFactory();
// This is the PolicyFactory used for the Birt Report Builder usage feature. ("REPORT" contentTypeId)
// It allows to use the OOTB Birt Report Builder example.
// You might need to enhance it for your needs but normally you should not
// In any case be sure to check https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet before changing things here...
public static final PolicyFactory BIRT_REPORT_BUILDER_USAGE_POLICY = new HtmlPolicyBuilder()
.allowWithoutAttributes("html", "body")
.allowElements("div", "span", "table", "tr", "td", "script")
.allowElements("form", "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", "onsubmit").onElements("form")
.toFactory();
}
public static class XmlEncoder implements SimpleEncoder {
private static final char[] IMMUNE_XML = {',', '.', '-', '_', ' '};
private XMLEntityCodec xmlCodec = new XMLEntityCodec();
public String encode(String original) {
if (original == null) {
return null;
}
return xmlCodec.encode(IMMUNE_XML, original);
}
/**
* @deprecated Use {@link #sanitize(String,String)} instead
*/
public String sanitize(String original) {
return sanitize(original, null);
}
public String sanitize(String original, String contentTypeId) {
return encode(original);
}
}
public static class UrlCodec implements SimpleEncoder, SimpleDecoder {
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
*/
public String sanitize(String original) {
return sanitize(original, null);
}
public String sanitize(String original, String contentTypeId) {
return encode(original);
}
public String decode(String original) {
try {
String canonical = canonicalize(original);
return URLDecoder.decode(canonical, "UTF-8");
} catch (UnsupportedEncodingException ee) {
Debug.logError(ee, module);
return null;
}
}
}
public static class StringEncoder implements SimpleEncoder {
public String encode(String original) {
if (original != null) {
original = original.replace("\"", "\\\"");
}
return original;
}
/**
* @deprecated Use {@link #sanitize(String,String)} instead
*/
public String sanitize(String original) {
return sanitize(original, null);
}
public String sanitize(String original, String contentTypeId) {
return encode(original);
}
}
// ================== Begin General Functions ==================
public static SimpleEncoder getEncoder(String type) {
if ("url".equals(type)) {
return urlCodec;
} else if ("xml".equals(type)) {
return xmlEncoder;
} else if ("html".equals(type)) {
return htmlEncoder;
} else if ("string".equals(type)) {
return stringEncoder;
} else {
return null;
}
}
public static SimpleDecoder getDecoder(String type) {
if ("url".equals(type)) {
return urlCodec;
} else {
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");
} else {
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");
} else {
Debug.logWarning("Multiple (" + foundCount + "x) encoding detected in " + input, module);
}
} else if (mixedCount > 1) {
if (restrictMixed) {
throw new IntrusionException("Input validation failure");
} else {
Debug.logWarning("Mixed encoding (" + mixedCount + "x) detected in " + input, module);
}
}
return working;
}
/**
* Uses a black-list approach for necessary characters for HTML.
* <p>
* Does not allow various characters (after canonicalization), including
* "&lt;", "&gt;", "&amp;" (if not followed by a space), and "%" (if not
* followed by a space).
*
* @param value
* @param errorMessageList
*/
public static String checkStringForHtmlStrictNone(String valueName, String value, List<String> errorMessageList) {
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);
errorMessageList.add("In field [" + valueName + "] found character escaping (mixed or double) that is not allowed or other format consistency error: " + e.toString());
}
// check for "<", ">"
if (value.indexOf("<") >= 0 || value.indexOf(">") >= 0) {
errorMessageList.add("In field [" + valueName + "] less-than (<) and greater-than (>) symbols are not allowed.");
}
// TODO: anything else to check for that can be used to get HTML or JavaScript going without these characters?
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<K>();
mapWrapper.setup(mapToWrap, encoder);
return mapWrapper;
}
protected Map<K, Object> internalMap = null;
protected SimpleEncoder encoder = null;
protected HtmlEncodingMapWrapper() { }
public void setup(Map<K, Object> mapToWrap, SimpleEncoder encoder) {
this.internalMap = mapToWrap;
this.encoder = encoder;
}
public void reset() {
this.internalMap = null;
this.encoder = null;
}
public int size() { return this.internalMap.size(); }
public boolean isEmpty() { return this.internalMap.isEmpty(); }
public boolean containsKey(Object key) { return this.internalMap.containsKey(key); }
public boolean containsValue(Object value) { return this.internalMap.containsValue(value); }
public Object get(Object key) {
Object theObject = this.internalMap.get(key);
if (theObject instanceof String) {
if (this.encoder != null) {
return encoder.encode((String) theObject);
} else {
return UtilCodec.getEncoder("html").encode((String) theObject);
}
} else if (theObject instanceof Map<?, ?>) {
return HtmlEncodingMapWrapper.getHtmlEncodingMapWrapper(UtilGenerics.<K, Object>checkMap(theObject), this.encoder);
}
return theObject;
}
public Object put(K key, Object value) { return this.internalMap.put(key, value); }
public Object remove(Object key) { return this.internalMap.remove(key); }
public void putAll(Map<? extends K, ? extends Object> arg0) { this.internalMap.putAll(arg0); }
public void clear() { this.internalMap.clear(); }
public Set<K> keySet() { return this.internalMap.keySet(); }
public Collection<Object> values() { return this.internalMap.values(); }
public Set<Map.Entry<K, Object>> entrySet() { return this.internalMap.entrySet(); }
@Override
public String toString() { return this.internalMap.toString(); }
}
}