This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.xss-1.0.10 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git
commit 7a68e9df1242ee81de1e9c3625dd23511973ce4e Author: Radu Cotescu <[email protected]> AuthorDate: Wed May 13 13:40:07 2015 +0000 SLING-4557 - Add JSON and XML validation to the XSS Protection API * Added validator methods to the API * Added JSON validation implementation using Apache Commons JSON * Added XML validation implementation using SAX * Added unit tests (applied slightly modified patch from Vlad Bailescu; closes #81) git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/xss@1679207 13f79535-47bb-0310-9956-ffa450edef68 --- src/main/java/org/apache/sling/xss/XSSAPI.java | 64 ++++++++---- .../java/org/apache/sling/xss/impl/XSSAPIImpl.java | 114 ++++++++++++++++++++- .../java/org/apache/sling/xss/package-info.java | 4 +- .../org/apache/sling/xss/impl/XSSAPIImplTest.java | 91 ++++++++++++++++ 4 files changed, 245 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/apache/sling/xss/XSSAPI.java b/src/main/java/org/apache/sling/xss/XSSAPI.java index b2ad26f..0e3ea2c 100644 --- a/src/main/java/org/apache/sling/xss/XSSAPI.java +++ b/src/main/java/org/apache/sling/xss/XSSAPI.java @@ -1,4 +1,5 @@ -/******************************************************************************* +/** + * **************************************************************************** * 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 @@ -6,14 +7,15 @@ * 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 - * + * <p/> * 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; @@ -52,7 +54,7 @@ public interface XSSAPI { * @return a sanitized integer */ @Nullable - public Integer getValidInteger(@Nullable String integer, @Nullable int defaultValue); + Integer getValidInteger(@Nullable String integer, int defaultValue); /** * Validate a string which should contain a long, returning a default value if the source is @@ -63,7 +65,7 @@ public interface XSSAPI { * @return a sanitized integer */ @Nullable - public Long getValidLong(@Nullable String source, @Nullable long defaultValue); + Long getValidLong(@Nullable String source,long defaultValue); /** * Validate a string which should contain a dimension, returning a default value if the source is @@ -74,7 +76,7 @@ public interface XSSAPI { * @return a sanitized dimension */ @Nullable - public String getValidDimension(@Nullable String dimension, @Nullable String defaultValue); + String getValidDimension(@Nullable String dimension, @Nullable String defaultValue); /** * Sanitizes a URL for writing as an HTML href or src attribute value. @@ -83,7 +85,7 @@ public interface XSSAPI { * @return a sanitized URL (possibly empty) */ @Nonnull - public String getValidHref(@Nullable String url); + String getValidHref(@Nullable String url); /** * Validate a Javascript token. The value must be either a single identifier, a literal number, @@ -94,7 +96,7 @@ public interface XSSAPI { * @return a string containing a single identifier, a literal number, or a literal string token */ @Nullable - public String getValidJSToken(@Nullable String token, @Nullable String defaultValue); + String getValidJSToken(@Nullable String token, @Nullable String defaultValue); /** * Validate a style/CSS token. Valid CSS tokens are specified at http://www.w3.org/TR/css3-syntax/ @@ -105,7 +107,7 @@ public interface XSSAPI { * @return a string containing sanitized style token */ @Nullable - public String getValidStyleToken(@Nullable String token, @Nullable String defaultValue); + String getValidStyleToken(@Nullable String token, @Nullable String defaultValue); /** * Validate a CSS color value. Color values as specified at http://www.w3.org/TR/css3-color/#colorunits @@ -117,17 +119,35 @@ public interface XSSAPI { * @return a string a css color value. */ @Nullable - public String getValidCSSColor(@Nullable String color, @Nullable String defaultColor); + String getValidCSSColor(@Nullable String color, @Nullable String defaultColor); /** - * Validate multiline comment to be used inside a <script>...</script> or <style>...</style> block. Multiline + * Validate multi-line comment to be used inside a <script>...</script> or <style>...</style> block. Multi-line * comment end block is disallowed * * @param comment the comment to be used * @param defaultComment a default value to use if the comment is {@code null} or not valid. - * @return a valid multiline comment + * @return a valid multi-line comment + */ + String getValidMultiLineComment(@Nullable String comment, @Nullable String defaultComment); + + /** + * Validate a JSON string + * + * @param json the JSON string to validate + * @param defaultJson the default value to use if {@code json} is {@code null} or not valid + * @return a valid JSON string + */ + String getValidJSON(@Nullable String json, @Nullable String defaultJson); + + /** + * Validate an XML string + * + * @param xml the XML string to validate + * @param defaultXml the default value to use if {@code xml} is {@code null} or not valid + * @return a valid XML string */ - public String getValidMultiLineComment(@Nullable String comment, @Nullable String defaultComment); + String getValidXML(@Nullable String xml, @Nullable String defaultXml); // ============================================================================================= // ENCODERS @@ -141,7 +161,7 @@ public interface XSSAPI { * @return an encoded version of the source */ @Nullable - public String encodeForHTML(@Nullable String source); + String encodeForHTML(@Nullable String source); /** * Encodes a source string for writing to an HTML attribute value. @@ -151,7 +171,7 @@ public interface XSSAPI { * @return an encoded version of the source */ @Nullable - public String encodeForHTMLAttr(@Nullable String source); + String encodeForHTMLAttr(@Nullable String source); /** * Encodes a source string for XML element content. @@ -161,7 +181,7 @@ public interface XSSAPI { * @return an encoded version of the source */ @Nullable - public String encodeForXML(@Nullable String source); + String encodeForXML(@Nullable String source); /** * Encodes a source string for writing to an XML attribute value. @@ -170,7 +190,7 @@ public interface XSSAPI { * @return an encoded version of the source */ @Nullable - public String encodeForXMLAttr(@Nullable String source); + String encodeForXMLAttr(@Nullable String source); /** * Encodes a source string for writing to JavaScript string content. @@ -181,7 +201,7 @@ public interface XSSAPI { * @return an encoded version of the source */ @Nullable - public String encodeForJSString(@Nullable String source); + String encodeForJSString(@Nullable String source); /** * Encodes a source string for writing to CSS string content. @@ -192,7 +212,7 @@ public interface XSSAPI { * @return an encoded version of the source */ @Nullable - public String encodeForCSSString(@Nullable String source); + String encodeForCSSString(@Nullable String source); // ============================================================================================= @@ -207,7 +227,7 @@ public interface XSSAPI { * @return a string containing the sanitized HTML which may be an empty string if {@code source} is {@code null} or empty */ @Nonnull - public String filterHTML(@Nullable String source); + String filterHTML(@Nullable String source); // ============================================================================================= @@ -221,7 +241,7 @@ public interface XSSAPI { * @param request the request from which to obtain the {@link org.apache.sling.xss.XSSAPI} * @return an XSSAPI service capable of validating hrefs. */ - public XSSAPI getRequestSpecificAPI(SlingHttpServletRequest request); + XSSAPI getRequestSpecificAPI(SlingHttpServletRequest request); /** * Returns an XSSAPI instance capable of mapping resource URLs. @@ -230,6 +250,6 @@ public interface XSSAPI { * @param resourceResolver the resolver from which to obtain the {@link org.apache.sling.xss.XSSAPI} * @return an XSSAPI service capable of validating hrefs. */ - public XSSAPI getResourceResolverSpecificAPI(ResourceResolver resourceResolver); + XSSAPI getResourceResolverSpecificAPI(ResourceResolver resourceResolver); } diff --git a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java index cb05449..e9f0db9 100644 --- a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java +++ b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java @@ -16,28 +16,39 @@ ******************************************************************************/ package org.apache.sling.xss.impl; +import java.io.StringReader; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.commons.json.JSONArray; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.commons.json.JSONObject; import org.apache.sling.xss.ProtectionContext; import org.apache.sling.xss.XSSAPI; import org.apache.sling.xss.XSSFilter; 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(value = XSSAPI.class) public class XSSAPIImpl implements XSSAPI { - - // ============================================================================================= - // VALIDATORS - // + private static final Logger LOGGER = LoggerFactory.getLogger(XSSAPIImpl.class); @Reference private XSSFilter xssFilter = null; @@ -46,9 +57,30 @@ public class XSSAPIImpl implements XSSAPI { private static final Pattern PATTERN_AUTO_DIMENSION = Pattern.compile("['\"]?auto['\"]?"); + private SAXParserFactory factory; + + @Activate + @SuppressWarnings("unused") + protected void activate() { + factory = SAXParserFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(true); + } + + @Deactivate + @SuppressWarnings("unused") + protected void deactivate() { + factory = 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 { @@ -65,6 +97,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#getValidLong(String, long) */ + @Override public Long getValidLong(String source, long defaultValue) { if (source != null && source.length() > 0) { try { @@ -83,6 +116,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @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()) { @@ -156,6 +190,8 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#getValidHref(String) */ + @Override + @Nonnull public String getValidHref(final String url) { if (url != null && url.length() > 0) { // Percent-encode characters that are not allowed in unquoted @@ -189,6 +225,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @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(); @@ -238,6 +275,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @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; @@ -249,6 +287,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @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(); @@ -272,6 +311,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#getValidMultiLineComment(String, String) */ + @Override public String getValidMultiLineComment(String comment, String defaultComment) { if (comment != null && !comment.contains("*/")) { return comment; @@ -279,6 +319,62 @@ public class XSSAPIImpl implements XSSAPI { 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 { + JSONObject obj = new JSONObject(json); + return obj.toString(); + } catch (JSONException e) { + LOGGER.debug("JSON validation failed: " + e.getMessage(), e); + } + } else { + try { + JSONArray arr = new JSONArray(json); + return arr.toString(); + } catch (JSONException 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 // @@ -286,6 +382,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#encodeForHTML(String) */ + @Override public String encodeForHTML(String source) { return source == null ? null : Encode.forHtml(source); } @@ -293,6 +390,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#encodeForHTMLAttr(String) */ + @Override public String encodeForHTMLAttr(String source) { return source == null ? null : Encode.forHtmlAttribute(source); } @@ -300,6 +398,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#encodeForXML(String) */ + @Override public String encodeForXML(String source) { return source == null ? null : Encode.forXml(source); } @@ -307,6 +406,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#encodeForXMLAttr(String) */ + @Override public String encodeForXMLAttr(String source) { return source == null ? null : Encode.forXmlAttribute(source); } @@ -314,6 +414,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#encodeForJSString(String) */ + @Override public String encodeForJSString(String source) { return source == null ? null : Encode.forJavaScript(source); } @@ -321,6 +422,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#encodeForCSSString(String) */ + @Override public String encodeForCSSString(String source) { return source == null ? null : Encode.forCssString(source); } @@ -332,6 +434,8 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#filterHTML(String) */ + @Override + @Nonnull public String filterHTML(String source) { return xssFilter.filter(ProtectionContext.HTML_HTML_CONTENT, source); } @@ -343,6 +447,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#getRequestSpecificAPI(org.apache.sling.api.SlingHttpServletRequest) */ + @Override public XSSAPI getRequestSpecificAPI(final SlingHttpServletRequest request) { return this; } @@ -350,6 +455,7 @@ public class XSSAPIImpl implements XSSAPI { /** * @see org.apache.sling.xss.XSSAPI#getResourceResolverSpecificAPI(org.apache.sling.api.resource.ResourceResolver) */ + @Override public XSSAPI getResourceResolverSpecificAPI(final ResourceResolver resourceResolver) { return this; } diff --git a/src/main/java/org/apache/sling/xss/package-info.java b/src/main/java/org/apache/sling/xss/package-info.java index 546d328..de16384 100644 --- a/src/main/java/org/apache/sling/xss/package-info.java +++ b/src/main/java/org/apache/sling/xss/package-info.java @@ -17,9 +17,9 @@ /** * XSS Protection Service * - * @version 1.0.0 + * @version 1.1.0 */ -@Version("1.0.0") +@Version("1.1.0") package org.apache.sling.xss; import aQute.bnd.annotation.Version; diff --git a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java index dce4391..d08cb3f 100644 --- a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java +++ b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java @@ -41,6 +41,8 @@ import static org.mockito.Mockito.when; public class XSSAPIImplTest { public static final String RUBBISH = "rubbish"; + public static final String RUBBISH_JSON = "[\"rubbish\"]"; + public static final String RUBBISH_XML = "<rubbish/>"; private XSSAPI xssAPI; @@ -59,6 +61,7 @@ public class XSSAPIImplTest { Whitebox.setInternalState(xssFilter, "defaultHandler", mockPolicyHandler); xssAPI = new XSSAPIImpl(); + Whitebox.invokeMethod(xssAPI, "activate"); Field filterField = XSSAPIImpl.class.getDeclaredField("xssFilter"); filterField.setAccessible(true); filterField.set(xssAPI, xssFilter); @@ -550,4 +553,92 @@ public class XSSAPIImplTest { } } } + + @Test + public void testGetValidJSON() { + String[][] testData = { + {null, RUBBISH_JSON}, + {"", ""}, + {"1]", RUBBISH_JSON}, + {"{}", "{}"}, + {"{1}", RUBBISH_JSON}, + { + "{test: 'test'}", + "{\"test\":\"test\"}" + }, + { + "{test:\"test}", + RUBBISH_JSON + }, + { + "{test1:'test1', test2: {test21: 'test21', test22: 'test22'}}", + "{\"test1\":\"test1\",\"test2\":{\"test21\":\"test21\",\"test22\":\"test22\"}}" + }, + {"[]", "[]"}, + {"[1,2]", "[1,2]"}, + {"[1", RUBBISH_JSON}, + { + "[{test: 'test'}]", + "[{\"test\":\"test\"}]" + } + }; + for (String[] aTestData : testData) { + String source = aTestData[0]; + String expected = aTestData[1]; + + String result = xssAPI.getValidJSON(source, RUBBISH_JSON); + if (!result.equals(expected)) { + fail("Validating JSON '" + source + "', expecting '" + expected + "', but got '" + result + "'"); + } + } + } + + @Test + public void testGetValidXML() { + String[][] testData = { + {null, RUBBISH_XML}, + {"", ""}, + { + "<t/>", + "<t/>" + }, + { + "<t>", + RUBBISH_XML + }, + { + "<t>test</t>", + "<t>test</t>" + }, + { + "<t>test", + RUBBISH_XML + }, + { + "<t t=\"t\">test</t>", + "<t t=\"t\">test</t>" + }, + { + "<t t=\"t>test</t>", + RUBBISH_XML + }, + { + "<t><w>xyz</w></t>", + "<t><w>xyz</w></t>" + }, + { + "<t><w>xyz</t></w>", + RUBBISH_XML + } + }; + for (String[] aTestData : testData) { + String source = aTestData[0]; + String expected = aTestData[1]; + + String result = xssAPI.getValidXML(source, RUBBISH_XML); + if (!result.equals(expected)) { + fail("Validating XML '" + source + "', expecting '" + expected + "', but got '" + result + "'"); + } + } + } } -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
