org.owasp.validator.html.Policy Maven / Gradle / Ivy
Show all versions of com.liferay.portal.security.antisamy
/*
* Copyright (c) 2007-2021, Arshan Dabirsiaghi, Jason Li, Kristian Rosenvold
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of OWASP nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.owasp.validator.html;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.owasp.validator.html.model.AntiSamyPattern;
import org.owasp.validator.html.model.Attribute;
import org.owasp.validator.html.model.Property;
import org.owasp.validator.html.model.Tag;
import org.owasp.validator.html.scan.Constants;
import org.owasp.validator.html.util.URIUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import static org.owasp.validator.html.util.XMLUtil.getAttributeValue;
/**
* Policy.java - This file holds the model for our policy engine.
*
* ## Schema validation behavior change starting with AntiSamy 1.6.0 ##
*
* Prior to v1.6.0 AntiSamy was not actually enforcing it's defined XSD. Now, by default AntiSamy enforce the schema,
* and won't continue if the AntiSamy policy is invalid. However, we recognize that it might not be possible for
* developers to fix their AntiSamy policies right away if they are non-compliant, and yet still want to upgrade
* AntiSamy to pick up any security improvements, feature enhancements, and bug fixes. As such, we now provide two
* ways to (temporarily!) disable schema validation:
*
* 1) Set the Java System property: owasp.validator.validateschema to false. This can be done at the command line
* (e.g., -Dowasp.validator.validateschema=false) or via the Java System properties file. Neither requires a code
* change.
*
* 2) Change the code using AntiSamy to invoke: Policy.setSchemaValidation(false) before loading the AntiSamy policy.
* This is a static call so once disabled, it is disabled for all new Policy instances.
*
* To encourage AntiSamy users to only use XSD compliant policies, AntiSamy will always log some type of warning
* when schema validation is disabled. It will either WARN that the policy is non-compliant so it can be fixed, or
* it will WARN that the policy is compliant, but schema validation is OFF, so validation should be turned back on
* (i.e., stop disabling it). We also added INFO level logging when AntiSamy schema's are loaded and validated.
*
* ## Disabling schema validation is deprecated immediately, and will go away in AntiSamy 1.7+ ##
*
* The ability to disable the new schema validation feature is intended to be temporary, to smooth the transition to
* properly valid AntiSamy policy files. We plan to drop this feature in the next major release. We estimate that
* this will be some time mid-late 2022, so not any time soon. The idea is to give dev teams using AntiSamy directly,
* or through other libraries like ESAPI, plenty of time to get their policy files schema compliant before schema
* validation becomes required.
*
* Logging: The logging introduced in 1.6+ uses slf4j. AntiSamy includes the slf4j-simple library for its logging,
* but AntiSamy users can import and use an alternate slf4j compatible logging library if they prefer. They can also
* then exclude slf4j-simple if they want to.
*
* WARNING:: AntiSamy's use of slf4j-simple, without any configuration file, logs messages in a buffered
* manner to standard output. As such, some or all of these log messages may get lost if an Exception, such as a
* PolicyException is thrown. This can likely be rectified by configuring slf4j-simple to log to standard error
* instead, or use an alternate slf4j logger that does so.
*
* @author Arshan Dabirsiaghi
*/
public class Policy {
protected static final Logger logger = LoggerFactory.getLogger(Policy.class);
public static final Pattern ANYTHING_REGEXP = Pattern.compile(".*", Pattern.DOTALL);
private static final String POLICY_SCHEMA_URI = "antisamy.xsd";
protected static final String DEFAULT_POLICY_URI = "antisamy.xml";
private static final String DEFAULT_ONINVALID = "removeAttribute";
public static final int DEFAULT_MAX_INPUT_SIZE = 100000;
public static final int DEFAULT_MAX_STYLESHEET_IMPORTS = 1;
public static final String OMIT_XML_DECLARATION = "omitXmlDeclaration";
public static final String OMIT_DOCTYPE_DECLARATION = "omitDoctypeDeclaration";
/** @deprecated XHTML usage will go away in AntiSamy 1.7+ */
@Deprecated
public static final String USE_XHTML = "useXHTML";
public static final String FORMAT_OUTPUT = "formatOutput";
public static final String EMBED_STYLESHEETS = "embedStyleSheets";
public static final String CONNECTION_TIMEOUT = "connectionTimeout";
public static final String ANCHORS_NOFOLLOW = "nofollowAnchors";
public static final String ANCHORS_NOOPENER_NOREFERRER = "noopenerAndNoreferrerAnchors";
public static final String VALIDATE_PARAM_AS_EMBED = "validateParamAsEmbed";
public static final String PRESERVE_SPACE = "preserveSpace";
public static final String PRESERVE_COMMENTS = "preserveComments";
public static final String ENTITY_ENCODE_INTL_CHARS = "entityEncodeIntlChars";
public static final String ALLOW_DYNAMIC_ATTRIBUTES = "allowDynamicAttributes";
public static final String MAX_INPUT_SIZE = "maxInputSize";
public static final String MAX_STYLESHEET_IMPORTS = "maxStyleSheetImports";
public static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
public static final String EXTERNAL_PARAM_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
public static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
public static final String LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
public static final String ACTION_VALIDATE = "validate";
public static final String ACTION_FILTER = "filter";
public static final String ACTION_TRUNCATE = "truncate";
private final Map commonRegularExpressions;
protected final Map tagRules;
protected final Map cssRules;
protected final Map directives;
private final Map globalAttributes;
private final Map dynamicAttributes;
private final TagMatcher allowedEmptyTagsMatcher;
private final TagMatcher requiresClosingTagsMatcher;
/**
* XML Schema for policy validation
*/
private static volatile Schema schema = null;
private static boolean validateSchema = true; // Default is to validate schemas
public static final String VALIDATIONPROPERTY = "owasp.validator.validateschema";
// Support the ability to change the default schema validation behavior by setting the
// System property "owasp.antisamy.validateschema".
static {
loadValidateSchemaProperty();
}
// encapsulated to be simulated from test cases
private static void loadValidateSchemaProperty() {
String validateProperty = System.getProperty(VALIDATIONPROPERTY);
if (validateProperty != null) {
setSchemaValidation(Boolean.parseBoolean(validateProperty));
logger.warn("Setting AntiSamy policy schema validation to '" + getSchemaValidation() + "' because '"
+ VALIDATIONPROPERTY + "' system property set to: '" + validateProperty
+ "'. Note: this feature is temporary and will go away in AntiSamy v1.7.0 (~mid/late 2022) when validation will become mandatory.");
} else validateSchema = true; // default (or back to default if invoked multiple times during testing)
}
/**
* Get the Tag specified by the provided tag name.
*
* @param tagName
* The name of the Tag to return.
* @return The requested Tag, or null if it doesn't exist.
*/
public Tag getTagByLowercaseName(String tagName) {
return tagRules.get(tagName);
}
protected static class ParseContext {
Map commonRegularExpressions = new HashMap();
Map commonAttributes = new HashMap();
Map tagRules = new HashMap();
Map cssRules = new HashMap();
Map directives = new HashMap();
Map globalAttributes = new HashMap();
Map dynamicAttributes = new HashMap();
List allowedEmptyTags = new ArrayList();
List requireClosingTags = new ArrayList();
public void resetParamsWhereLastConfigWins() {
allowedEmptyTags.clear();
requireClosingTags.clear();
}
}
/**
* Retrieves a CSS Property from the Policy.
*
* @param propertyName The name of the CSS Property to look up.
* @return The CSS Property associated with the name specified, or null if none is found.
*/
public Property getPropertyByName(String propertyName) {
return cssRules.get(propertyName.toLowerCase());
}
/**
* Is XSD schema validation across all policies enabled or not? It is enabled by default.
*
* @return True if schema validation enabled. False otherwise.
*
* @deprecated Temporary method to enable AntiSamy users to upgrade to 1.6.x while still using policy files that aren't
* schema compliant. AntiSamy plans to make schema validation mandatory starting with v1.7.0 (~mid/late 2022).
*/
@Deprecated
public static boolean getSchemaValidation() {
return validateSchema;
}
/**
* This can enable/disable the schema validation against AntiSamy XSD for the instantiated
* policies. It is enabled by default.
*
* @param enable boolean value to specify if the schema validation should be performed. Use false to disable.
*
* @deprecated Temporary method to enable AntiSamy users to upgrade to 1.6.x while still using policy files that aren't
* schema compliant. AntiSamy plans to make schema validation mandatory starting with v1.7.0 (~mid/late 2022).
*/
@Deprecated
public static void setSchemaValidation(boolean enable) {
validateSchema = enable;
}
/**
* Construct a Policy using the default policy file location ("antisamy.xml").
*
* @return A populated Policy object based on the XML policy file located in the default location.
* @throws PolicyException If the file is not found or there is a problem parsing the file.
*/
public static Policy getInstance() throws PolicyException {
return getInstance(Policy.class.getClassLoader().getResource(DEFAULT_POLICY_URI));
}
/**
* Construct a Policy based on the file whose name is passed in.
*
* @param filename The path to the XML policy file.
* @return A populated Policy object based on the XML policy file located in the location passed in.
* @throws PolicyException If the file is not found or there is a problem parsing the file.
*/
public static Policy getInstance(String filename) throws PolicyException {
File file = new File(filename);
return getInstance(file);
}
/**
* Construct a Policy from the InputStream object passed in.
*
* @param inputStream An InputStream which contains the XML policy information.
* @return A populated Policy object based on the XML policy file pointed to by the inputStream parameter.
* @throws PolicyException If there is a problem parsing the input stream.
*/
public static Policy getInstance(InputStream inputStream) throws PolicyException {
final String logMsg = "Attempting to load AntiSamy policy from an input stream.";
// If schema validation is disabled, we elevate this msg to the warn level to match the
// level of the mandatory warning that will follow. We do the same below.
if (validateSchema) logger.info(logMsg); else logger.warn(logMsg);
return new InternalPolicy(getSimpleParseContext(getTopLevelElement(inputStream)));
}
/**
* Construct a Policy from the File object passed in.
*
* @param file A File object which contains the XML policy information.
* @return A populated Policy object based on the XML policy file pointed to by the File parameter.
* @throws PolicyException If the file is not found or there is a problem parsing the file.
*/
public static Policy getInstance(File file) throws PolicyException {
try {
URI uri = file.toURI();
return getInstance(uri.toURL());
} catch (IOException e) {
throw new PolicyException(e);
}
}
/**
* Construct a Policy from the target of the URL passed in.
*
* NOTE: This is the only factory method that will work with <include> tags
* in AntiSamy policy files.
*
* For security reasons, the provided URL must point to a local file. Currently only 'file:' and 'jar:'
* URL prefixes are allowed. If you want to use a different URL format, and are confident that the URL
* points to a safe source, you can open the target of the URL with URL.openStream(), and use the
* getInstance(InputStream) constructor instead. For example, Spring has classpath: and Wildfly/Jboss
* supports vfs: for accessing local files. Just be aware that this alternate constructor doesn't support
* the use of <include> tags, per the NOTE above.
*
* @param url A URL object which contains the XML policy information.
* @return A populated Policy object based on the XML policy file pointed to by the File parameter.
* @throws PolicyException If the file is not found or there is a problem parsing the file.
*/
public static Policy getInstance(URL url) throws PolicyException {
String logMsg = "Attempting to load AntiSamy policy from URL: " + url.toString();
if (validateSchema) logger.info(logMsg); else logger.warn(logMsg);
return new InternalPolicy(getParseContext(getTopLevelElement(url), url));
}
protected Policy(ParseContext parseContext) {
this.allowedEmptyTagsMatcher = new TagMatcher(parseContext.allowedEmptyTags);
this.requiresClosingTagsMatcher = new TagMatcher(parseContext.requireClosingTags);
this.commonRegularExpressions = Collections.unmodifiableMap(parseContext.commonRegularExpressions);
this.tagRules = Collections.unmodifiableMap(parseContext.tagRules);
this.cssRules = Collections.unmodifiableMap(parseContext.cssRules);
this.directives = Collections.unmodifiableMap(parseContext.directives);
this.globalAttributes = Collections.unmodifiableMap(parseContext.globalAttributes);
this.dynamicAttributes = Collections.unmodifiableMap(parseContext.dynamicAttributes);
}
protected Policy(Policy old, Map directives, Map tagRules, Map cssRules) {
this.allowedEmptyTagsMatcher = old.allowedEmptyTagsMatcher;
this.requiresClosingTagsMatcher = old.requiresClosingTagsMatcher;
this.commonRegularExpressions = old.commonRegularExpressions;
this.tagRules = tagRules;
this.cssRules = cssRules;
this.directives = directives;
this.globalAttributes = old.globalAttributes;
this.dynamicAttributes = old.dynamicAttributes;
}
protected static ParseContext getSimpleParseContext(Element topLevelElement) throws PolicyException {
ParseContext parseContext = new ParseContext();
if (getByTagName(topLevelElement, "include").iterator().hasNext()) {
throw new IllegalArgumentException(
"A policy file loaded with an InputStream cannot contain include references");
}
parsePolicy(topLevelElement, parseContext);
return parseContext;
}
protected static ParseContext getParseContext(Element topLevelElement, URL baseUrl) throws PolicyException {
ParseContext parseContext = new ParseContext();
/**
* Are there any included policies? These are parsed here first so that
* rules in _this_ policy file will override included rules.
*
* NOTE that by this being here we only support one level of includes.
* To support recursion, move this into the parsePolicy method.
*/
for (Element include : getByTagName(topLevelElement, "include")) {
String href = getAttributeValue(include, "href");
Element includedPolicy = getPolicy(href, baseUrl);
parsePolicy(includedPolicy, parseContext);
}
parsePolicy(topLevelElement, parseContext);
return parseContext;
}
protected static Element getTopLevelElement(final URL baseUrl) throws PolicyException {
final InputSource source = getSourceFromUrl(baseUrl);
return getTopLevelElement(source, new Callable() {
@Override
public InputSource call() throws PolicyException {
return getSourceFromUrl(baseUrl);
}
});
}
@SuppressFBWarnings(value = "SECURITY", justification="Opening a stream to the provided URL is not "
+ "a vulnerability because it points to a local JAR file.")
protected static InputSource getSourceFromUrl(URL baseUrl) throws PolicyException {
try {
InputSource source = resolveEntity(baseUrl.toExternalForm(), baseUrl);
if (source == null) {
source = new InputSource(baseUrl.toExternalForm());
source.setByteStream(baseUrl.openStream());
} else {
source.setSystemId(baseUrl.toExternalForm());
}
return source;
} catch (SAXException | IOException e) {
// SAXException can't actually happen. See JavaDoc for resolveEntity(String, URL)
throw new PolicyException(e);
}
}
private static Element getTopLevelElement(InputStream is) throws PolicyException {
final InputSource source = new InputSource(toByteArrayStream(is));
return getTopLevelElement(source, new Callable() {
@Override
public InputSource call() throws IOException {
source.getByteStream().reset();
return source;
}
});
}
protected static Element getTopLevelElement(InputSource source, Callable getResetSource) throws PolicyException {
// Track whether an exception was ever thrown while processing policy file
Exception thrownException = null;
try {
return getDocumentElementFromSource(source, true);
} catch (SAXException e) {
thrownException = e;
if (!validateSchema) {
try {
source = getResetSource.call();
Element theElement = getDocumentElementFromSource(source, false);
// We warn when the policy has an invalid schema, but schema validation is disabled.
logger.warn("Invalid AntiSamy policy file: " + e.getMessage());
return theElement;
} catch (Exception e2) {
throw new PolicyException(e2);
}
} else throw new PolicyException(e);
} catch (ParserConfigurationException | IOException e) {
thrownException = e;
throw new PolicyException(e);
} finally {
if (!validateSchema && thrownException == null) {
// We warn when the policy has a valid schema, but schema validation is disabled.
logger.warn("XML schema validation is disabled for a valid AntiSamy policy. Please reenable policy validation.");
}
}
}
/*
* This method takes an arbitrary input stream, copies its contents into a byte[], then returns it
* in a ByteArrayInputStream, closing the provided InputStream in the process. It's purpose is to
* ensure that the InputStream we are using can be reset to the beginning, as not all InputStream's properly
* allow this. We use this for AntiSamy XML policy files, which we never expect to get that large
* (e.g., a few Kb at most).
*/
private static InputStream toByteArrayStream(InputStream in) throws PolicyException {
byte[] byteArray;
try (Reader reader = new InputStreamReader(in, Charset.forName("UTF8"))) {
char[] charArray = new char[8 * 1024];
StringBuilder builder = new StringBuilder();
int numCharsRead;
while ((numCharsRead = reader.read(charArray, 0, charArray.length)) != -1) {
builder.append(charArray, 0, numCharsRead);
}
byteArray = builder.toString().getBytes(Charset.forName("UTF8"));
} catch (IOException ioe) {
throw new PolicyException(ioe);
}
return new ByteArrayInputStream(byteArray);
}
private static Element getDocumentElementFromSource(InputSource source, boolean schemaValidationEnabled)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
/**
* Disable external entities, etc.
*/
dbf.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
dbf.setFeature(EXTERNAL_PARAM_ENTITIES, false);
dbf.setFeature(DISALLOW_DOCTYPE_DECL, true);
dbf.setFeature(LOAD_EXTERNAL_DTD, false);
if (schemaValidationEnabled) {
getPolicySchema();
dbf.setNamespaceAware(true);
dbf.setSchema(schema);
}
DocumentBuilder db = dbf.newDocumentBuilder();
db.setErrorHandler(new SAXErrorHandler());
Document dom = db.parse(source);
return dom.getDocumentElement();
}
private static void parsePolicy(Element topLevelElement, ParseContext parseContext)
throws PolicyException {
if (topLevelElement == null) return;
parseContext.resetParamsWhereLastConfigWins();
parseCommonRegExps(getFirstChild(topLevelElement, "common-regexps"), parseContext.commonRegularExpressions);
parseDirectives(getFirstChild(topLevelElement, "directives"), parseContext.directives);
parseCommonAttributes(getFirstChild(topLevelElement, "common-attributes"), parseContext.commonAttributes, parseContext.commonRegularExpressions);
parseGlobalAttributes(getFirstChild(topLevelElement, "global-tag-attributes"), parseContext.globalAttributes, parseContext.commonAttributes);
parseDynamicAttributes(getFirstChild(topLevelElement, "dynamic-tag-attributes"), parseContext.dynamicAttributes, parseContext.commonAttributes);
parseTagRules(getFirstChild(topLevelElement, "tag-rules"), parseContext.commonAttributes, parseContext.commonRegularExpressions, parseContext.tagRules);
parseCSSRules(getFirstChild(topLevelElement, "css-rules"), parseContext.cssRules, parseContext.commonRegularExpressions);
parseAllowedEmptyTags(getFirstChild(topLevelElement, "allowed-empty-tags"), parseContext.allowedEmptyTags);
parseRequireClosingTags(getFirstChild(topLevelElement, "require-closing-tags"), parseContext.requireClosingTags);
}
/**
* Returns the top level element of a loaded policy Document
*/
@SuppressFBWarnings(value = "SECURITY", justification="Opening a stream to the provided URL is not "
+ "a vulnerability because only local file URLs are allowed.")
private static Element getPolicy(String href, URL baseUrl) throws PolicyException {
// Track whether an exception was ever thrown while processing policy file
Exception thrownException = null;
try {
return getDocumentElementByUrl(href, baseUrl, true);
} catch (SAXException e) {
thrownException = e;
if (!validateSchema) {
try {
Element theElement = getDocumentElementByUrl(href, baseUrl, false);
// We warn when the policy has an invalid schema, but schema validation is disabled.
logger.warn("Invalid AntiSamy policy file: " + e.getMessage());
return theElement;
} catch (SAXException | ParserConfigurationException | IOException e2) {
throw new PolicyException(e2);
}
} else {
throw new PolicyException(e);
}
} catch (ParserConfigurationException | IOException e) {
thrownException = e;
throw new PolicyException(e);
} finally {
if (!validateSchema && thrownException == null) {
// We warn when the policy has a valid schema, but schema validation is disabled.
logger.warn("XML schema validation is disabled for a valid AntiSamy policy. Please reenable policy validation.");
}
}
}
// TODO: Add JavaDocs for this new method.
@SuppressFBWarnings(value = "SECURITY", justification="Opening a stream to the provided URL is not "
+ "a vulnerability because only local file URLs are allowed.")
private static Element getDocumentElementByUrl(String href, URL baseUrl, boolean schemaValidationEnabled)
throws IOException, ParserConfigurationException, SAXException {
InputSource source = null;
// Can't resolve public id, but might be able to resolve relative
// system id, since we have a base URI.
if (href != null && baseUrl != null) {
verifyLocalUrl(baseUrl);
URL url;
try {
url = new URL(baseUrl, href);
final String logMsg = "Attempting to load AntiSamy policy from URL: " + url.toString();
if (validateSchema) logger.info(logMsg); else logger.warn(logMsg);
source = new InputSource(url.openStream());
source.setSystemId(href);
} catch (MalformedURLException | FileNotFoundException e) {
try {
String absURL = URIUtils.resolveAsString(href, baseUrl.toString());
url = new URL(absURL);
source = new InputSource(url.openStream());
source.setSystemId(href);
} catch (MalformedURLException ex2) {
// nothing to do
// TODO: Is this true? Or should we at least log the original exception, or
// rethrow it?
}
}
}
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
/**
* Disable external entities, etc.
*/
dbf.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
dbf.setFeature(EXTERNAL_PARAM_ENTITIES, false);
dbf.setFeature(DISALLOW_DOCTYPE_DECL, true);
dbf.setFeature(LOAD_EXTERNAL_DTD, false);
// This code doesn't have the retry logic if schema validation fails because schemaValidationEnabled is
// passed in. It is up to the caller to try again, if this fails the first time (if they want to).
if (schemaValidationEnabled) {
getPolicySchema();
dbf.setNamespaceAware(true);
dbf.setSchema(schema);
}
DocumentBuilder db = dbf.newDocumentBuilder();
db.setErrorHandler(new SAXErrorHandler());
// Load and parse the file.
if (source != null) {
Document dom = db.parse(source);
// Get the policy information out of it!
return dom.getDocumentElement();
}
return null;
}
/**
* Creates a copy of this policy with an added/changed directive.
* @param name The directive to add/modify
* @param value The value
* @return A clone of the policy with the updated directive
*/
public Policy cloneWithDirective(String name, String value) {
Map directives = new HashMap(this.directives);
directives.put(name, value);
return new InternalPolicy(this, Collections.unmodifiableMap(directives), tagRules, cssRules);
}
/**
* Go through section of the policy file.
*
* @param root Top level of
* @param directives The directives map to update
*/
private static void parseDirectives(Element root, Map directives) {
for (Element ele : getByTagName(root, "directive")) {
String name = getAttributeValue(ele, "name");
String value = getAttributeValue(ele, "value");
directives.put(name, value);
}
}
/**
* Go through section of the policy file.
*
* @param allowedEmptyTagsListNode Top level of
* @param allowedEmptyTags The tags that can be empty
*/
private static void parseAllowedEmptyTags(Element allowedEmptyTagsListNode,
List allowedEmptyTags) {
if (allowedEmptyTagsListNode != null) {
for (Element literalNode :
getGrandChildrenByTagName(allowedEmptyTagsListNode, "literal-list", "literal")) {
String value = getAttributeValue(literalNode, "value");
if (value != null && value.length() > 0) {
allowedEmptyTags.add(value);
}
}
} else allowedEmptyTags.addAll(Constants.defaultAllowedEmptyTags);
}
/**
* Go through section of the policy file.
*
* @param requireClosingTagsListNode Top level of
* @param requireClosingTags The list of tags that require closing
*/
private static void parseRequireClosingTags(Element requireClosingTagsListNode,
List requireClosingTags) {
if (requireClosingTagsListNode != null) {
for (Element literalNode :
getGrandChildrenByTagName(requireClosingTagsListNode, "literal-list", "literal")) {
String value = getAttributeValue(literalNode, "value");
if (value != null && value.length() > 0) {
requireClosingTags.add(value);
}
}
} else requireClosingTags.addAll(Constants.defaultRequireClosingTags);
}
/**
* Go through section of the policy file.
*
* @param root Top level of
* @param globalAttributes1 A HashMap of global Attributes that need validation for every tag.
* @param commonAttributes The common attributes
* @throws PolicyException
*/
private static void parseGlobalAttributes(Element root, Map globalAttributes1, Map commonAttributes) throws PolicyException {
for (Element ele : getByTagName(root, "attribute")) {
String name = getAttributeValue(ele, "name");
Attribute toAdd = commonAttributes.get(name.toLowerCase());
if (toAdd != null) globalAttributes1.put(name.toLowerCase(), toAdd);
else throw new PolicyException("Global attribute '" + name
+ "' was not defined in ");
}
}
/**
* Go through section of the policy file.
*
* @param root Top level of
* @param dynamicAttributes A HashMap of dynamic Attributes that need validation for every tag.
* @param commonAttributes The common attributes
* @throws PolicyException
*/
private static void parseDynamicAttributes(Element root, Map dynamicAttributes, Map commonAttributes) throws PolicyException {
for (Element ele : getByTagName(root, "attribute")) {
String name = getAttributeValue(ele, "name");
Attribute toAdd = commonAttributes.get(name.toLowerCase());
if (toAdd != null) {
String attrName = name.toLowerCase().substring(0, name.length() - 1);
dynamicAttributes.put(attrName, toAdd);
} else throw new PolicyException("Dynamic attribute '" + name
+ "' was not defined in ");
}
}
/**
* Go through the section of the policy file.
*
* @param root Top level of
* @param commonRegularExpressions1 the antisamy pattern objects
*/
private static void parseCommonRegExps(Element root, Map commonRegularExpressions1) {
for (Element ele : getByTagName(root, "regexp")) {
String name = getAttributeValue(ele, "name");
Pattern pattern = Pattern.compile(getAttributeValue(ele, "value"), Pattern.DOTALL);
commonRegularExpressions1.put(name, new AntiSamyPattern(pattern));
}
}
private static void parseCommonAttributes(Element root, Map commonAttributes1,
Map commonRegularExpressions1) {
for (Element ele : getByTagName(root, "attribute")) {
String onInvalid = getAttributeValue(ele, "onInvalid");
String name = getAttributeValue(ele, "name");
List allowedRegexps = getAllowedRegexps(commonRegularExpressions1, ele);
List allowedValues = getAllowedLiterals(ele);
final String onInvalidStr;
if (onInvalid != null && onInvalid.length() > 0) {
onInvalidStr = onInvalid;
} else onInvalidStr = DEFAULT_ONINVALID;
String description = getAttributeValue(ele, "description");
Attribute attribute = new Attribute(getAttributeValue(ele, "name"), allowedRegexps,
allowedValues, onInvalidStr, description);
commonAttributes1.put(name.toLowerCase(), attribute);
}
}
private static List getAllowedLiterals(Element ele) {
List allowedValues = new ArrayList();
for (Element literalNode : getGrandChildrenByTagName(ele, "literal-list", "literal")) {
String value = getAttributeValue(literalNode, "value");
if (value != null && value.length() > 0) {
allowedValues.add(value);
} else if (literalNode.getNodeValue() != null) {
allowedValues.add(literalNode.getNodeValue());
}
}
return allowedValues;
}
private static List getAllowedRegexps(Map commonRegularExpressions1, Element ele) {
List allowedRegExp = new ArrayList();
for (Element regExpNode : getGrandChildrenByTagName(ele, "regexp-list", "regexp")) {
String regExpName = getAttributeValue(regExpNode, "name");
String value = getAttributeValue(regExpNode, "value");
if (regExpName != null && regExpName.length() > 0) {
allowedRegExp.add(commonRegularExpressions1.get(regExpName).getPattern());
} else allowedRegExp.add(Pattern.compile(value, Pattern.DOTALL));
}
return allowedRegExp;
}
private static List getAllowedRegexps2(Map commonRegularExpressions1,
Element attributeNode, String tagName) throws PolicyException {
List allowedRegexps = new ArrayList();
for (Element regExpNode : getGrandChildrenByTagName(attributeNode, "regexp-list", "regexp")) {
String regExpName = getAttributeValue(regExpNode, "name");
String value = getAttributeValue(regExpNode, "value");
/*
* Look up common regular expression specified
* by the "name" field. They can put a common
* name in the "name" field or provide a custom
* value in the "value" field. They must choose
* one or the other, not both.
*/
if (regExpName != null && regExpName.length() > 0) {
AntiSamyPattern pattern = commonRegularExpressions1.get(regExpName);
if (pattern != null) {
allowedRegexps.add(pattern.getPattern());
} else throw new PolicyException("Regular expression '" + regExpName +
"' was referenced as a common regexp in definition of '" + tagName +
"', but does not exist in ");
} else if (value != null && value.length() > 0) {
allowedRegexps.add(Pattern.compile(value, Pattern.DOTALL));
}
}
return allowedRegexps;
}
private static List getAllowedRegexp3(Map commonRegularExpressions1,
Element ele, String name) throws PolicyException {
List allowedRegExp = new ArrayList();
for (Element regExpNode : getGrandChildrenByTagName(ele, "regexp-list", "regexp")) {
String regExpName = getAttributeValue(regExpNode, "name");
String value = getAttributeValue(regExpNode, "value");
AntiSamyPattern pattern = commonRegularExpressions1.get(regExpName);
if (pattern != null) {
allowedRegExp.add(pattern.getPattern());
} else if (value != null) {
allowedRegExp.add(Pattern.compile(value, Pattern.DOTALL));
} else throw new PolicyException("Regular expression '" + regExpName +
"' was referenced as a common regexp in definition of '" + name +
"', but does not exist in ");
}
return allowedRegExp;
}
private static void parseTagRules(Element root, Map commonAttributes1, Map commonRegularExpressions1, Map tagRules1) throws PolicyException {
if (root == null) return;
for (Element tagNode : getByTagName(root, "tag")) {
String name = getAttributeValue(tagNode, "name");
String action = getAttributeValue(tagNode, "action");
NodeList attributeList = tagNode.getElementsByTagName("attribute");
Map tagAttributes = getTagAttributes(commonAttributes1, commonRegularExpressions1, attributeList, name);
Tag tag = new Tag(name, tagAttributes, action);
tagRules1.put(name.toLowerCase(), tag);
}
}
private static Map getTagAttributes(Map commonAttributes1, Map commonRegularExpressions1, NodeList attributeList, String tagName) throws PolicyException {
Map tagAttributes = new HashMap();
for (int j = 0; j < attributeList.getLength(); j++) {
Element attributeNode = (Element) attributeList.item(j);
String attrName = getAttributeValue(attributeNode, "name").toLowerCase();
if (!attributeNode.hasChildNodes()) {
Attribute attribute = commonAttributes1.get(attrName);
// All they provided was the name, so they must want a common attribute.
if (attribute != null) {
/*
* If they provide onInvalid/description values here they will
* override the common values.
*/
String onInvalid = getAttributeValue(attributeNode, "onInvalid");
String description = getAttributeValue(attributeNode, "description");
Attribute changed = attribute.mutate(onInvalid, description);
commonAttributes1.put(attrName, changed);
tagAttributes.put(attrName, changed);
} else throw new PolicyException("Attribute '" + getAttributeValue(attributeNode, "name") +
"' was referenced as a common attribute in definition of '" + tagName +
"', but does not exist in ");
} else {
List allowedRegexps2 = getAllowedRegexps2(commonRegularExpressions1, attributeNode, tagName);
List allowedValues2 = getAllowedLiterals(attributeNode);
String onInvalid = getAttributeValue(attributeNode, "onInvalid");
String description = getAttributeValue(attributeNode, "description");
Attribute attribute = new Attribute(getAttributeValue(attributeNode, "name"), allowedRegexps2, allowedValues2, onInvalid, description);
// Add fully built attribute.
tagAttributes.put(attrName, attribute);
}
}
return tagAttributes;
}
private static void parseCSSRules(Element root, Map cssRules1, Map commonRegularExpressions1) throws PolicyException {
for (Element ele : getByTagName(root, "property")) {
String name = getAttributeValue(ele, "name");
String description = getAttributeValue(ele, "description");
List allowedRegexp3 = getAllowedRegexp3(commonRegularExpressions1, ele, name);
List allowedValue = new ArrayList();
for (Element literalNode : getGrandChildrenByTagName(ele, "literal-list", "literal")) {
allowedValue.add(getAttributeValue(literalNode, "value"));
}
List shortHandRefs = new ArrayList();
for (Element shorthandNode : getGrandChildrenByTagName(ele, "shorthand-list", "shorthand")) {
shortHandRefs.add(getAttributeValue(shorthandNode, "name"));
}
String onInvalid = getAttributeValue(ele, "onInvalid");
final String onInvalidStr;
if (onInvalid != null && onInvalid.length() > 0) {
onInvalidStr = onInvalid;
} else onInvalidStr = DEFAULT_ONINVALID;
Property property = new Property(name,allowedRegexp3, allowedValue, shortHandRefs, description, onInvalidStr);
cssRules1.put(name.toLowerCase(), property);
}
}
/**
* A simple method for returning on of the <global-attribute> entries by
* name.
*
* @param name The name of the global-attribute we want to look up.
* @return An Attribute associated with the global-attribute lookup name specified.
*/
public Attribute getGlobalAttributeByName(String name) {
return globalAttributes.get(name.toLowerCase());
}
/**
* A method for returning one of the dynamic <global-attribute> entries by name.
*
* @param name The name of the dynamic global-attribute we want to look up.
* @return An Attribute associated with the global-attribute lookup name specified,
* or null if not found.
*/
public Attribute getDynamicAttributeByName(String name) {
Attribute dynamicAttribute = null;
Set> entries = dynamicAttributes.entrySet();
for (Map.Entry entry : entries) {
if (name.startsWith(entry.getKey())) {
dynamicAttribute = entry.getValue();
break;
}
}
return dynamicAttribute;
}
/**
* Return all the allowed empty tags configured in the Policy.
*
* @return A String array of all the he allowed empty tags configured in the Policy.
*/
public TagMatcher getAllowedEmptyTags() {
return allowedEmptyTagsMatcher;
}
/**
* Return all the tags that are required to be closed with an end tag, even if they have no child content.
*
* @return A String array of all the tags that are required to be closed with an end tag, even if they have no child content.
*/
public TagMatcher getRequiresClosingTags() {
return requiresClosingTagsMatcher;
}
/**
* Return a directive value based on a lookup name.
*
* @param name The name of the directive we want to look up.
* @return A String object containing the directive associated with the lookup name, or null if none is found.
*/
public String getDirective(String name) {
return directives.get(name);
}
/**
* Resolves public and system IDs to files stored within the JAR.
*
* @param systemId The name of the entity we want to look up.
* @param baseUrl The base location of the entity.
* @return A String object containing the directive associated with the lookup name, or null if none is found.
* @throws IOException if the specified URL can't be opened.
* @throws SAXException This exception can't actually be thrown, but left in the method signature for
* API compatibility reasons.
*/
@SuppressFBWarnings(value = "SECURITY", justification="Opening a stream to the provided URL is not "
+ "a vulnerability because only local file URLs are allowed.")
public static InputSource resolveEntity(final String systemId, URL baseUrl) throws IOException, SAXException {
InputSource source;
// Can't resolve public id, but might be able to resolve relative
// system id, since we have a base URI.
if (systemId != null && baseUrl != null) {
verifyLocalUrl(baseUrl);
URL url;
try {
url = new URL(baseUrl, systemId);
source = new InputSource(url.openStream());
source.setSystemId(systemId);
return source;
} catch (MalformedURLException | FileNotFoundException e) {
try {
String absURL = URIUtils.resolveAsString(systemId, baseUrl.toString());
url = new URL(absURL);
source = new InputSource(url.openStream());
source.setSystemId(systemId);
return source;
} catch (MalformedURLException ex2) {
// nothing to do
}
}
return null;
}
// No resolving.
return null;
}
/**
* Verify that the target of the URL is a local file only. Currently, we allow file: and jar: URLs.
* The target of the URL is typically an AntiSamy policy file.
* @param url The URL to verify.
* @throws MalformedURLException If the supplied URL does not reference a local file directly, or one inside
* a local JAR file.
*/
private static void verifyLocalUrl(URL url) throws MalformedURLException {
switch (url.getProtocol()) {
case "file":
case "jar" : break; // These are OK.
default: throw new MalformedURLException(
"Only local files can be accessed with a policy URL. Illegal value supplied was: " + url);
}
}
private static Element getFirstChild(Element element, String tagName) {
if (element == null) return null;
NodeList elementsByTagName = element.getElementsByTagName(tagName);
if (elementsByTagName != null && elementsByTagName.getLength() > 0)
return (Element) elementsByTagName.item(0);
else return null;
}
private static Iterable getGrandChildrenByTagName(Element parent, String immediateChildName, String subChild){
NodeList elementsByTagName = parent.getElementsByTagName(immediateChildName);
if (elementsByTagName.getLength() == 0) return Collections.emptyList();
Element regExpListNode = (Element) elementsByTagName.item(0);
return getByTagName( regExpListNode, subChild);
}
private static Iterable getByTagName(Element parent, String tagName) {
if (parent == null) return Collections.emptyList();
final NodeList nodes = parent.getElementsByTagName(tagName);
return new Iterable() {
public Iterator iterator() {
return new Iterator() {
int pos = 0;
int len = nodes.getLength();
public boolean hasNext() {
return pos < len;
}
public Element next() {
return (Element) nodes.item(pos++);
}
public void remove() {
throw new UnsupportedOperationException("Cant remove");
}
};
}
};
}
public AntiSamyPattern getCommonRegularExpressions(String name) {
return commonRegularExpressions.get(name);
}
private static void getPolicySchema() throws SAXException {
if (schema == null) {
InputStream schemaStream = Policy.class.getClassLoader().getResourceAsStream(POLICY_SCHEMA_URI);
Source schemaSource = new StreamSource(schemaStream);
schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
.newSchema(schemaSource);
}
}
/**
* This class is implemented to just throw an exception when
* validating the policy schema while parsing the document.
*/
static class SAXErrorHandler implements ErrorHandler {
@Override
public void error(SAXParseException arg0) throws SAXException {
throw arg0;
}
@Override
public void fatalError(SAXParseException arg0) throws SAXException {
throw arg0;
}
@Override
public void warning(SAXParseException arg0) throws SAXException {
throw arg0;
}
}
}