| /******************************************************************************* |
| * 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.sling.xss.impl; |
| |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URLDecoder; |
| import java.nio.charset.StandardCharsets; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.Nonnull; |
| import javax.json.Json; |
| import javax.json.JsonReaderFactory; |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| import org.apache.commons.lang3.StringEscapeUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.sling.xss.ProtectionContext; |
| import org.apache.sling.xss.XSSAPI; |
| import org.apache.sling.xss.XSSFilter; |
| import org.osgi.framework.Constants; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.component.annotations.Deactivate; |
| import org.osgi.service.component.annotations.Reference; |
| import org.owasp.encoder.Encode; |
| import org.owasp.esapi.ESAPI; |
| import org.owasp.esapi.Validator; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.XMLReader; |
| |
| @Component(service = XSSAPI.class, |
| property = { |
| Constants.SERVICE_VENDOR + "=The Apache Software Foundation" |
| }) |
| |
| public class XSSAPIImpl implements XSSAPI { |
| |
| private final Logger LOGGER = LoggerFactory.getLogger(XSSAPIImpl.class); |
| |
| @Reference |
| private XSSFilter xssFilter; |
| |
| private final Validator validator = ESAPI.validator(); |
| |
| private static final Pattern PATTERN_AUTO_DIMENSION = Pattern.compile("['\"]?auto['\"]?"); |
| |
| private SAXParserFactory factory; |
| |
| private volatile JsonReaderFactory jsonReaderFactory; |
| |
| @Activate |
| protected void activate() { |
| factory = SAXParserFactory.newInstance(); |
| factory.setValidating(false); |
| factory.setNamespaceAware(true); |
| try { |
| factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); |
| factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); |
| factory.setFeature("http://xml.org/sax/features/external-general-entities", false); |
| } catch (Exception e) { |
| LOGGER.error("SAX parser configuration error: " + e.getMessage(), e); |
| } |
| Map<String, Object> config = new HashMap<>(); |
| config.put("org.apache.johnzon.supports-comments", true); |
| jsonReaderFactory = Json.createReaderFactory(config); |
| } |
| |
| @Deactivate |
| protected void deactivate() { |
| factory = null; |
| jsonReaderFactory = null; |
| } |
| |
| // ============================================================================================= |
| // VALIDATORS |
| // |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidInteger(String, int) |
| */ |
| @Override |
| public Integer getValidInteger(String integer, int defaultValue) { |
| if (integer != null && integer.length() > 0) { |
| try { |
| return validator.getValidInteger("XSS", integer, -2000000000, 2000000000, false); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| |
| // fall through to default if empty, null, or validation failure |
| return defaultValue; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidLong(String, long) |
| */ |
| @Override |
| public Long getValidLong(String source, long defaultValue) { |
| if (source != null && source.length() > 0) { |
| try { |
| LongValidationRule ivr = new LongValidationRule( "number", ESAPI.encoder(), -9000000000000000000L, 9000000000000000000L ); |
| ivr.setAllowNull(false); |
| return ivr.getValid("XSS", source); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| |
| // fall through to default if empty, null, or validation failure |
| return defaultValue; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidDouble(String, double) |
| */ |
| @Override |
| public Double getValidDouble(String source, double defaultValue) { |
| if (source != null && source.length() > 0) { |
| try { |
| return validator.getValidDouble("XSS", source, 0d, Double.MAX_VALUE, false); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| |
| // fall through to default if empty, null, or validation failure |
| return defaultValue; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidDimension(String, String) |
| */ |
| @Override |
| public String getValidDimension(String dimension, String defaultValue) { |
| if (dimension != null && dimension.length() > 0) { |
| if (PATTERN_AUTO_DIMENSION.matcher(dimension).matches()) { |
| return "\"auto\""; |
| } |
| |
| try { |
| return validator.getValidInteger("XSS", dimension, -10000, 10000, false).toString(); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| |
| // fall through to default if empty, null, or validation failure |
| return defaultValue; |
| } |
| |
| private static final String MANGLE_NAMESPACE_OUT_SUFFIX = ":"; |
| |
| private static final String MANGLE_NAMESPACE_OUT = "/([^:/]+):"; |
| |
| private static final Pattern MANGLE_NAMESPACE_PATTERN = Pattern.compile(MANGLE_NAMESPACE_OUT); |
| |
| private static final String MANGLE_NAMESPACE_IN_SUFFIX = "_"; |
| |
| private static final String MANGLE_NAMESPACE_IN_PREFIX = "/_"; |
| |
| private static final String SCHEME_PATTERN = "://"; |
| |
| private String mangleNamespaces(String absPath) { |
| if (absPath != null) { |
| // check for absolute urls |
| final int schemeIndex = absPath.indexOf(SCHEME_PATTERN); |
| final String manglePath; |
| final String prefix; |
| if (schemeIndex != -1) { |
| final int pathIndex = absPath.indexOf("/", schemeIndex + 3); |
| if (pathIndex != -1) { |
| prefix = absPath.substring(0, pathIndex); |
| manglePath = absPath.substring(pathIndex); |
| } else { |
| prefix = absPath; |
| manglePath = ""; |
| } |
| } else { |
| prefix = ""; |
| manglePath = absPath; |
| } |
| if (manglePath.contains(MANGLE_NAMESPACE_OUT_SUFFIX)) { |
| final Matcher m = MANGLE_NAMESPACE_PATTERN.matcher(manglePath); |
| |
| final StringBuffer buf = new StringBuffer(); |
| while (m.find()) { |
| final String replacement = MANGLE_NAMESPACE_IN_PREFIX + m.group(1) + MANGLE_NAMESPACE_IN_SUFFIX; |
| m.appendReplacement(buf, replacement); |
| } |
| |
| m.appendTail(buf); |
| |
| absPath = prefix + buf.toString(); |
| |
| } |
| } |
| |
| return absPath; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidHref(String) |
| */ |
| @Override |
| @Nonnull |
| public String getValidHref(final String url) { |
| if (StringUtils.isNotEmpty(url)) { |
| try { |
| String unescapedURL = URLDecoder.decode(url, StandardCharsets.UTF_8.name()); |
| /* |
| StringEscapeUtils is deprecated starting with version 3.6 of commons-lang3, however the indicated replacement comes from |
| commons-text, which is not an OSGi bundle |
| */ |
| unescapedURL = StringEscapeUtils.unescapeXml(unescapedURL); |
| // Percent-encode characters that are not allowed in unquoted |
| // HTML attributes: ", ', >, <, ` and space. We don't encode = |
| // since this would break links with query parameters. |
| String encodedUrl = unescapedURL.replaceAll("\"", "%22") |
| .replaceAll("'", "%27") |
| .replaceAll(">", "%3E") |
| .replaceAll("<", "%3C") |
| .replaceAll("`", "%60") |
| .replaceAll(" ", "%20"); |
| int qMarkIx = encodedUrl.indexOf('?'); |
| if (qMarkIx > 0) { |
| encodedUrl = encodedUrl.substring(0, qMarkIx) + encodedUrl.substring(qMarkIx).replaceAll(":", "%3A"); |
| } |
| |
| encodedUrl = mangleNamespaces(encodedUrl); |
| if (xssFilter.isValidHref(encodedUrl)) { |
| return encodedUrl; |
| } |
| } catch (UnsupportedEncodingException e) { |
| LOGGER.error("Unable to decode url: {}.", url); |
| } |
| } |
| // fall through to empty string |
| return ""; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidJSToken(String, String) |
| */ |
| @Override |
| public String getValidJSToken(String token, String defaultValue) { |
| if (token != null && token.length() > 0) { |
| token = token.trim(); |
| String q = token.substring(0, 1); |
| if (q.matches("['\"]") && token.endsWith(q)) { |
| String literal = token.substring(1, token.length() - 1); |
| return q + encodeForJSString(literal) + q; |
| } else if (token.matches("[0-9a-zA-Z_$][0-9a-zA-Z_$.]*")) { |
| return token; |
| } |
| } |
| |
| // fall through to default value |
| return defaultValue; |
| } |
| |
| private static final String NON_ASCII = "\\x00\\x08\\x0B\\x0C\\x0E-\\x1F"; |
| /** http://www.w3.org/TR/css-syntax-3/#number-token-diagram */ |
| private static final String NUMBER = "[+-]?[\\d]*[\\.]?[\\d]*(?:[e][+-]?\\d+)?"; |
| /** http://www.w3.org/TR/css-syntax-3/#hex-digit-diagram */ |
| private static final String HEX_DIGITS = "#[0-9a-f]*"; |
| /** http://www.w3.org/TR/css-syntax-3/#ident-token-diagram */ |
| private static final String IDENTIFIER = "-?[a-z_" + NON_ASCII + "][\\w_\\-" + NON_ASCII + "]*"; |
| /** http://www.w3.org/TR/css-syntax-3/#string-token-diagram */ |
| private static final String STRING = "\"(?:(?!javascript\\s?:)[^\"^\\\\^\\n]|(?:\\\\\"))*\"|'(?:(?!javascript\\s?:)[^'^\\\\^\\n]|(?:\\\\'))*'"; |
| /** http://www.w3.org/TR/css-syntax-3/#dimension-token-diagram */ |
| private static final String DIMENSION = NUMBER + IDENTIFIER; |
| /** http://www.w3.org/TR/css-syntax-3/#percentage-token-diagram */ |
| private static final String PERCENT = NUMBER + "%"; |
| /** http://www.w3.org/TR/css-syntax-3/#function-token-diagram */ |
| private static final String FUNCTION = IDENTIFIER + "\\((?:(?:" + NUMBER + ")|(?:" + IDENTIFIER + ")|(?:[\\s]*)|(?:,))*\\)"; |
| /** http://www.w3.org/TR/css-syntax-3/#url-unquoted-diagram */ |
| private static final String URL_UNQUOTED = "[^\"^'^\\(^\\)^[" + NON_ASCII + "]]*"; |
| /** http://www.w3.org/TR/css-syntax-3/#url-token-diagram */ |
| private static final String URL = "url\\((?:(?:" + URL_UNQUOTED + ")|(?:" + STRING + "))\\)"; |
| /** composite regular expression for style token validation */ |
| private static final String CSS_TOKEN = "(?i)" // case insensitive |
| + "(?:" + NUMBER + ")" |
| + "|(?:" + DIMENSION + ")" |
| + "|(?:" + PERCENT + ")" |
| + "|(?:" + HEX_DIGITS + ")" |
| + "|(?:" + IDENTIFIER + ")" |
| + "|(?:" + STRING + ")" |
| + "|(?:" + FUNCTION + ")" |
| + "|(?:" + URL + ")"; |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidStyleToken(String, String) |
| */ |
| @Override |
| public String getValidStyleToken(String token, String defaultValue) { |
| if (token != null && token.length() > 0 && token.matches(CSS_TOKEN)) { |
| return token; |
| } |
| |
| return defaultValue; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidCSSColor(String, String) |
| */ |
| @Override |
| public String getValidCSSColor(String color, String defaultColor) { |
| if (color != null && color.length() > 0) { |
| color = color.trim(); |
| /* |
| * Avoid security implications by including only the characters required to specify colors in hex |
| * or functional notation. Critical characters disallowed: x (as in expression(...)), |
| * u (as in url(...)) and semi colon (as in escaping the context of the color value). |
| */ |
| if (color.matches("(?i)[#a-fghlrs(+0-9-.%,) \\t\\n\\x0B\\f\\r]+")) { |
| return color; |
| } |
| // named color values |
| if (color.matches("(?i)[a-zA-Z \\t\\n\\x0B\\f\\r]+")) { |
| return color; |
| } |
| } |
| |
| return defaultColor; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidMultiLineComment(String, String) |
| */ |
| @Override |
| public String getValidMultiLineComment(String comment, String defaultComment) { |
| if (comment != null && !comment.contains("*/")) { |
| return comment; |
| } |
| return defaultComment; |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidJSON(String, String) |
| */ |
| @Override |
| public String getValidJSON(String json, String defaultJson) { |
| if (json == null) { |
| return getValidJSON(defaultJson, ""); |
| } |
| json = json.trim(); |
| if ("".equals(json)) { |
| return ""; |
| } |
| int curlyIx = json.indexOf("{"); |
| int straightIx = json.indexOf("["); |
| if (curlyIx >= 0 && (curlyIx < straightIx || straightIx < 0)) { |
| try { |
| StringWriter output = new StringWriter(); |
| Json.createGenerator(output).write(jsonReaderFactory.createReader(new StringReader(json)).readObject()).close(); |
| return output.getBuffer().toString(); |
| } catch (Exception e) { |
| LOGGER.debug("JSON validation failed: " + e.getMessage(), e); |
| } |
| } else { |
| try { |
| StringWriter output = new StringWriter(); |
| Json.createGenerator(output).write(jsonReaderFactory.createReader(new StringReader(json)).readArray()).close(); |
| return output.getBuffer().toString(); |
| } catch (Exception e) { |
| LOGGER.debug("JSON validation failed: " + e.getMessage(), e); |
| } |
| } |
| return getValidJSON(defaultJson, ""); |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#getValidXML(String, String) |
| */ |
| @Override |
| public String getValidXML(String xml, String defaultXml) { |
| if (xml == null) { |
| return getValidXML(defaultXml, ""); |
| } |
| xml = xml.trim(); |
| if ("".equals(xml)) { |
| return ""; |
| } |
| |
| try { |
| SAXParser parser = factory.newSAXParser(); |
| XMLReader reader = parser.getXMLReader(); |
| reader.parse(new InputSource(new StringReader(xml))); |
| return xml; |
| } catch (Exception e) { |
| LOGGER.debug("XML validation failed: " + e.getMessage(), e); |
| } |
| return getValidXML(defaultXml, ""); |
| } |
| |
| // ============================================================================================= |
| // ENCODERS |
| // |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#encodeForHTML(String) |
| */ |
| @Override |
| public String encodeForHTML(String source) { |
| return source == null ? null : Encode.forHtml(source); |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#encodeForHTMLAttr(String) |
| */ |
| @Override |
| public String encodeForHTMLAttr(String source) { |
| return source == null ? null : Encode.forHtmlAttribute(source); |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#encodeForXML(String) |
| */ |
| @Override |
| public String encodeForXML(String source) { |
| return source == null ? null : Encode.forXml(source); |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#encodeForXMLAttr(String) |
| */ |
| @Override |
| public String encodeForXMLAttr(String source) { |
| return source == null ? null : Encode.forXmlAttribute(source); |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#encodeForJSString(String) |
| */ |
| @Override |
| public String encodeForJSString(String source) { |
| return source == null ? null : Encode.forJavaScript(source).replace("\\-", "\\u002D"); |
| } |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#encodeForCSSString(String) |
| */ |
| @Override |
| public String encodeForCSSString(String source) { |
| return source == null ? null : Encode.forCssString(source); |
| } |
| |
| // ============================================================================================= |
| // FILTERS |
| // |
| |
| /** |
| * @see org.apache.sling.xss.XSSAPI#filterHTML(String) |
| */ |
| @Override |
| @Nonnull |
| public String filterHTML(String source) { |
| return xssFilter.filter(ProtectionContext.HTML_HTML_CONTENT, source); |
| } |
| } |