All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.owasp.validator.html.Policy Maven / Gradle / Ivy

Go to download

A library for performing fast, configurable cleansing of HTML coming from untrusted sources.

The newest version!
/*
 * Copyright (c) 2007-2022, 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 static org.owasp.validator.html.util.XMLUtil.getAttributeValue;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
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;

/**
 * This class 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. For all of v1.6.x, by * default AntiSamy enforced the schema, and wouldn't continue if the AntiSamy policy was invalid. * However, we recognized that it might not be possible for developers to fix their AntiSamy * policies right away so we provided two ways to (temporarily!) disable schema validation. Via a * direct method call and via a System property. * *

## Starting with AntiSamy 1.7.0, schema validation is Mandatory. * *

Logging: The logging introduced in 1.6+ uses slf4j. But AntiSamy doesn't actually include an * slf4j implementation library. AntiSamy users must import and properly configure an slf4j logging * library if they want to see the very few log messages generated by AntiSamy. * * @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 String OMIT_XML_DECLARATION = "omitXmlDeclaration"; public static final String OMIT_DOCTYPE_DECLARATION = "omitDoctypeDeclaration"; public static final String FORMAT_OUTPUT = "formatOutput"; 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"; /** @deprecated Remote styles import feature to be removed and along with this error message. */ @Deprecated public static final int DEFAULT_MAX_STYLESHEET_IMPORTS = 1; /** @deprecated Remote styles import feature to be removed and along with this error message. */ @Deprecated public static final String EMBED_STYLESHEETS = "embedStyleSheets"; /** @deprecated Remote styles import feature to be removed and along with this error message. */ @Deprecated public static final String CONNECTION_TIMEOUT = "connectionTimeout"; /** @deprecated Remote styles import feature to be removed and along with this error message. */ @Deprecated 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"; public static final String ACTION_ENCODE = "encode"; 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; /** * 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()); } /** * 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 { logger.info("Attempting to load AntiSamy policy from an input stream."); 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 { logger.info("Attempting to load AntiSamy policy from URL: " + url.toString()); 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 try { return getDocumentElementFromSource(source); } catch (SAXException | ParserConfigurationException | IOException e) { throw new PolicyException(e); } } /* * 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) 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); // Schema validation is always required now. So turn it on. 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 try { return getDocumentElementByUrl(href, baseUrl); } catch (SAXException | ParserConfigurationException | IOException e) { throw new PolicyException(e); } } // 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) 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); logger.info("Attempting to load AntiSamy policy from URL: " + url.toString()); 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. It is up to the caller // to try again, // if this fails the first time (if they want to). 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() { @Override public Iterator iterator() { return new Iterator() { int pos = 0; int len = nodes.getLength(); @Override public boolean hasNext() { return pos < len; } @Override public Element next() { return (Element) nodes.item(pos++); } @Override 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; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy