/******************************************************************************* * 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.scripting.sightly.impl.engine.extension; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; import org.apache.sling.scripting.sightly.SightlyException; import org.apache.sling.scripting.sightly.compiler.RuntimeFunction; import org.apache.sling.scripting.sightly.compiler.expression.MarkupContext; import org.apache.sling.scripting.sightly.extension.RuntimeExtension; import org.apache.sling.scripting.sightly.render.RenderContext; import org.apache.sling.xss.XSSAPI; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Runtime support for XSS filtering */ @Component( service = RuntimeExtension.class, property = { RuntimeExtension.NAME + "=" + RuntimeFunction.XSS } ) public class XSSRuntimeExtension implements RuntimeExtension { @Reference(policyOption = ReferencePolicyOption.GREEDY) private XSSAPI xssApi; private static final Set<String> elementNameWhiteList = new HashSet<>(); private static final Logger LOG = LoggerFactory.getLogger(XSSRuntimeExtension.class); private static final Pattern VALID_ATTRIBUTE = Pattern.compile("^[a-zA-Z_:][\\-a-zA-Z0-9_:\\.]*$"); private static final Pattern ATTRIBUTE_BLACKLIST = Pattern.compile("^(style|(on.*))$", Pattern.CASE_INSENSITIVE); @Override public Object call(final RenderContext renderContext, Object... arguments) { if (arguments.length < 2) { throw new SightlyException( String.format("Extension %s requires at least %d arguments", RuntimeFunction.XSS, 2)); } Object original = arguments[0]; Object option = arguments[1]; Object hint = null; if (arguments.length >= 3) { hint = arguments[2]; } MarkupContext markupContext = null; if (option != null && option instanceof String) { String name = (String) option; markupContext = MarkupContext.lookup(name); } if (markupContext == MarkupContext.UNSAFE) { return original; } if (markupContext == null) { LOG.warn("Expression context {} is invalid, expression will be replaced by the empty string", option); return ""; } String text = renderContext.getObjectModel().toString(original); return applyXSSFilter(text, hint, markupContext); } private String applyXSSFilter(String text, Object hint, MarkupContext xssContext) { if (xssContext.equals(MarkupContext.ATTRIBUTE) && hint instanceof String) { String attributeName = (String) hint; MarkupContext attrMarkupContext = getAttributeMarkupContext(attributeName); return applyXSSFilter(text, attrMarkupContext); } return applyXSSFilter(text, xssContext); } private String applyXSSFilter(String text, MarkupContext xssContext) { switch (xssContext) { case ATTRIBUTE: return xssApi.encodeForHTMLAttr(text); case COMMENT: case TEXT: return xssApi.encodeForHTML(text); case ATTRIBUTE_NAME: return escapeAttributeName(text); case NUMBER: Long result = xssApi.getValidLong(text, 0); if (result != null) { return result.toString(); } case URI: return xssApi.getValidHref(text); case SCRIPT_TOKEN: return xssApi.getValidJSToken(text, ""); case STYLE_TOKEN: return xssApi.getValidStyleToken(text, ""); case SCRIPT_STRING: return xssApi.encodeForJSString(text); case STYLE_STRING: return xssApi.encodeForCSSString(text); case SCRIPT_COMMENT: case STYLE_COMMENT: return xssApi.getValidMultiLineComment(text, ""); case ELEMENT_NAME: return escapeElementName(text); case HTML: return xssApi.filterHTML(text); } return text; //todo: apply the rest of XSS filters } private String escapeElementName(String original) { original = original.trim(); if (elementNameWhiteList.contains(original.toLowerCase())) { return original; } return ""; } private MarkupContext getAttributeMarkupContext(String attributeName) { if ("src".equalsIgnoreCase(attributeName) || "href".equalsIgnoreCase(attributeName)) { return MarkupContext.URI; } return MarkupContext.ATTRIBUTE; } private String escapeAttributeName(String attributeName) { if (attributeName == null) { return null; } attributeName = attributeName.trim(); if (matchPattern(VALID_ATTRIBUTE, attributeName) && !isSensitiveAttribute(attributeName)) { return attributeName; } return null; } private boolean matchPattern(Pattern pattern, String str) { return pattern.matcher(str).matches(); } static { elementNameWhiteList.add("section"); elementNameWhiteList.add("nav"); elementNameWhiteList.add("article"); elementNameWhiteList.add("aside"); elementNameWhiteList.add("h1"); elementNameWhiteList.add("h2"); elementNameWhiteList.add("h3"); elementNameWhiteList.add("h4"); elementNameWhiteList.add("h5"); elementNameWhiteList.add("h6"); elementNameWhiteList.add("header"); elementNameWhiteList.add("footer"); elementNameWhiteList.add("address"); elementNameWhiteList.add("main"); elementNameWhiteList.add("p"); elementNameWhiteList.add("pre"); elementNameWhiteList.add("blockquote"); elementNameWhiteList.add("ul"); elementNameWhiteList.add("ol"); elementNameWhiteList.add("li"); elementNameWhiteList.add("dl"); elementNameWhiteList.add("dt"); elementNameWhiteList.add("dd"); elementNameWhiteList.add("figure"); elementNameWhiteList.add("figcaption"); elementNameWhiteList.add("div"); elementNameWhiteList.add("a"); elementNameWhiteList.add("em"); elementNameWhiteList.add("strong"); elementNameWhiteList.add("small"); elementNameWhiteList.add("s"); elementNameWhiteList.add("cite"); elementNameWhiteList.add("q"); elementNameWhiteList.add("dfn"); elementNameWhiteList.add("abbbr"); elementNameWhiteList.add("data"); elementNameWhiteList.add("time"); elementNameWhiteList.add("code"); elementNameWhiteList.add("var"); elementNameWhiteList.add("samp"); elementNameWhiteList.add("kbd"); elementNameWhiteList.add("sub"); elementNameWhiteList.add("sup"); elementNameWhiteList.add("i"); elementNameWhiteList.add("b"); elementNameWhiteList.add("u"); elementNameWhiteList.add("mark"); elementNameWhiteList.add("ruby"); elementNameWhiteList.add("rt"); elementNameWhiteList.add("rp"); elementNameWhiteList.add("bdi"); elementNameWhiteList.add("bdo"); elementNameWhiteList.add("span"); elementNameWhiteList.add("br"); elementNameWhiteList.add("wbr"); elementNameWhiteList.add("ins"); elementNameWhiteList.add("del"); elementNameWhiteList.add("table"); elementNameWhiteList.add("caption"); elementNameWhiteList.add("colgroup"); elementNameWhiteList.add("col"); elementNameWhiteList.add("tbody"); elementNameWhiteList.add("thead"); elementNameWhiteList.add("tfoot"); elementNameWhiteList.add("tr"); elementNameWhiteList.add("td"); elementNameWhiteList.add("th"); } private boolean isSensitiveAttribute(String name) { return ATTRIBUTE_BLACKLIST.matcher(name).matches(); } }