/******************************************************************************* * 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.FileInputStream; import java.io.InputStream; import java.lang.reflect.Field; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.xss.XSSAPI; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.owasp.validator.html.AntiSamy; import org.owasp.validator.html.Policy; import org.owasp.validator.html.model.Attribute; import org.powermock.reflect.Whitebox; import junit.framework.TestCase; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; 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; @Before public void setup() { try { XSSFilterImpl xssFilter = new XSSFilterImpl(); setDefaultHandler(xssFilter, "./src/main/resources/SLING-INF/content/config.xml"); xssAPI = new XSSAPIImpl(); Whitebox.invokeMethod(xssAPI, "activate"); Field filterField = XSSAPIImpl.class.getDeclaredField("xssFilter"); filterField.setAccessible(true); filterField.set(xssAPI, xssFilter); ResourceResolver mockResolver = mock(ResourceResolver.class); when(mockResolver.map(anyString())).thenAnswer(new Answer() { public Object answer(InvocationOnMock invocation) { Object[] args = invocation.getArguments(); String url = (String) args[0]; return url.replaceAll("jcr:", "_jcr_"); } }); SlingHttpServletRequest mockRequest = mock(SlingHttpServletRequest.class); when(mockRequest.getResourceResolver()).thenReturn(mockResolver); xssAPI = xssAPI.getRequestSpecificAPI(mockRequest); } catch (Exception e) { e.printStackTrace(); } } private static void setDefaultHandler(XSSFilterImpl xssFilter, String filename) throws Exception { InputStream policyStream = new FileInputStream(filename); Policy policy = Policy.getInstance(policyStream); AntiSamy antiSamy = new AntiSamy(policy); PolicyHandler mockPolicyHandler = mock(PolicyHandler.class); when(mockPolicyHandler.getPolicy()).thenReturn(policy); when(mockPolicyHandler.getAntiSamy()).thenReturn(antiSamy); Whitebox.invokeMethod(xssFilter, "setDefaultHandler", mockPolicyHandler); } @Test public void testEncodeForHTML() { String[][] testData = { // Source Expected Result // {null, null}, {"simple", "simple"}, {"<script>", "<script>"}, {"<b>", "<b>"}, {"günter", "günter"}, {"\u30e9\u30c9\u30af\u30ea\u30d5\u3001\u30de\u30e9\u30bd\u30f3\u4e94\u8f2a\u4ee3\u8868\u306b1\u4e07m\u51fa\u5834\u306b\u3082\u542b\u307f", "\u30e9\u30c9\u30af\u30ea\u30d5\u3001\u30de\u30e9\u30bd\u30f3\u4e94\u8f2a\u4ee3\u8868\u306b1\u4e07m\u51fa\u5834\u306b\u3082\u542b\u307f"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("HTML Encoding '" + source + "'", expected, xssAPI.encodeForHTML(source)); } } @Test public void testEncodeForHTMLAttr() { String[][] testData = { // Source Expected Result // {null, null}, {"simple", "simple"}, {"<script>", "<script>"}, {"\" <script>alert('pwned');</script>", "" <script>alert('pwned');</script>"}, {"günter", "günter"}, {"\u30e9\u30c9\u30af\u30ea\u30d5\u3001\u30de\u30e9\u30bd\u30f3\u4e94\u8f2a\u4ee3\u8868\u306b1\u4e07m\u51fa\u5834\u306b\u3082\u542b\u307f", "\u30e9\u30c9\u30af\u30ea\u30d5\u3001\u30de\u30e9\u30bd\u30f3\u4e94\u8f2a\u4ee3\u8868\u306b1\u4e07m\u51fa\u5834\u306b\u3082\u542b\u307f"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("HTML Encoding '" + source + "'", expected, xssAPI.encodeForHTMLAttr(source)); } } @Test public void testEncodeForXML() { String[][] testData = { // Source Expected Result // {null, null}, {"simple", "simple"}, {"<script>", "<script>"}, {"<b>", "<b>"}, {"günter", "günter"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("XML Encoding '" + source + "'", expected, xssAPI.encodeForXML(source)); } } @Test public void testEncodeForXMLAttr() { String[][] testData = { // Source Expected Result // {null, null}, {"simple", "simple"}, {"<script>", "<script>"}, {"<b>", "<b>"}, {"günter", "günter"}, {"\"xss:expression(alert('XSS'))", ""xss:expression(alert('XSS'))"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("XML Encoding '" + source + "'", expected, xssAPI.encodeForXMLAttr(source)); } } @Test public void testFilterHTML() { String[][] testData = { // Source Expected Result {null, ""}, {"", ""}, {"simple", "simple"}, {"<script>ugly</script>", ""}, {"<b>wow!</b>", "<b>wow!</b>"}, {"<p onmouseover='ugly'>nice</p>", "<p>nice</p>"}, {"<img src='javascript:ugly'/>", ""}, {"<img src='nice.jpg'/>", "<img src=\"nice.jpg\" />"}, {"<ul><li>1</li><li>2</li></ul>", "<ul><li>1</li><li>2</li></ul>"}, {"günter", "günter"}, {"<strike>strike</strike>", "<strike>strike</strike>"}, {"<s>s</s>", "<s>s</s>"}, {"<a href=\"\">empty href</a>", "<a href=\"\">empty href</a>"}, {"<a href=\" javascript:alert(23)\">space</a>","<a>space</a>"}, {"<table background=\"http://www.google.com\"></table>", "<table></table>"}, }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("Filtering '" + source + "'", expected, xssAPI.filterHTML(source)); } } @Test public void testGetValidHref() { String[][] testData = { // Href Expected Result // {"/etc/commerce/collections/中文", "/etc/commerce/collections/中文"}, {"/etc/commerce/collections/\u09aa\u09b0\u09c0\u0995\u09cd\u09b7\u09be\u09ae\u09c2\u09b2\u0995", "/etc/commerce/collections/\u09aa\u09b0\u09c0\u0995\u09cd\u09b7\u09be\u09ae\u09c2\u09b2\u0995"}, {null, ""}, {"", ""}, {"simple", "simple"}, {"../parent", "../parent"}, {"repo/günter", "repo/günter"}, // JCR namespaces: {"my/page/jcr:content.feed", "my/page/_jcr_content.feed"}, {"my/jcr:content/page/jcr:content", "my/_jcr_content/page/_jcr_content"}, {"\" onClick=ugly", "%22%20onClick=ugly"}, {"javascript:ugly", ""}, {"http://localhost:4502", "http://localhost:4502"}, {"http://localhost:4502/test", "http://localhost:4502/test"}, {"http://localhost:4502/jcr:content/test", "http://localhost:4502/_jcr_content/test"}, {"http://localhost:4502/test.html?a=b&b=c", "http://localhost:4502/test.html?a=b&b=c"}, // space {"/test/ab cd", "/test/ab%20cd"}, {"http://localhost:4502/test/ab cd", "http://localhost:4502/test/ab%20cd"}, {"/test/ab attr=c", "/test/ab%20attr=c"}, {"http://localhost:4502/test/ab attr=c", "http://localhost:4502/test/ab%20attr=c"}, // " {"/test/ab\"cd", "/test/ab%22cd"}, {"http://localhost:4502/test/ab\"cd", "http://localhost:4502/test/ab%22cd"}, // ' {"/test/ab'cd", "/test/ab%27cd"}, {"http://localhost:4502/test/ab'cd", "http://localhost:4502/test/ab%27cd"}, // = {"/test/ab=cd", "/test/ab=cd"}, {"http://localhost:4502/test/ab=cd", "http://localhost:4502/test/ab=cd"}, // > {"/test/ab>cd", "/test/ab%3Ecd"}, {"http://localhost:4502/test/ab>cd", "http://localhost:4502/test/ab%3Ecd"}, // < {"/test/ab<cd", "/test/ab%3Ccd"}, {"http://localhost:4502/test/ab<cd", "http://localhost:4502/test/ab%3Ccd"}, // ` {"/test/ab`cd", "/test/ab%60cd"}, {"http://localhost:4502/test/ab`cd", "http://localhost:4502/test/ab%60cd"}, // colons in query string {"/test/search.html?0_tag:id=test", "/test/search.html?0_tag%3Aid=test"}, { // JCR namespaces and colons in query string "/test/jcr:content/search.html?0_tag:id=test", "/test/_jcr_content/search.html?0_tag%3Aid=test" }, { // ? in query string "/test/search.html?0_tag:id=test?ing&1_tag:id=abc", "/test/search.html?0_tag%3Aid=test?ing&1_tag%3Aid=abc", } }; for (String[] aTestData : testData) { String href = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("Requested '" + href + "'", expected, xssAPI.getValidHref(href)); } } @Test public void testGetValidHrefWithoutHrefConfig() throws Exception { // Load AntiSamy configuration without href filter XSSFilterImpl xssFilter = Whitebox.getInternalState(xssAPI, "xssFilter"); setDefaultHandler(xssFilter, "./src/test/resources/configWithoutHref.xml"); Attribute hrefAttribute = Whitebox.getInternalState(xssFilter, "hrefAttribute"); Assert.assertEquals(hrefAttribute, XSSFilterImpl.DEFAULT_HREF_ATTRIBUTE); // Run same tests again to check default configuration testGetValidHref(); } @Test public void testGetValidInteger() { String[][] testData = { // Source Expected Result // {null, "123"}, {"100", "100"}, {"0", "0"}, {"junk", "123"}, {"100.5", "123"}, {"", "123"}, {"null", "123"} }; for (String[] aTestData : testData) { String source = aTestData[0]; Integer expected = (aTestData[1] != null) ? new Integer(aTestData[1]) : null; TestCase.assertEquals("Validating integer '" + source + "'", expected, xssAPI.getValidInteger(source, 123)); } } @Test public void testGetValidLong() { String[][] testData = { // Source Expected Result // {null, "123"}, {"100", "100"}, {"0", "0"}, {"junk", "123"}, {"100.5", "123"}, {"", "123"}, {"null", "123"} }; for (String[] aTestData : testData) { String source = aTestData[0]; Long expected = (aTestData[1] != null) ? new Long(aTestData[1]) : null; TestCase.assertEquals("Validating long '" + source + "'", expected, xssAPI.getValidLong(source, 123)); } } @Test public void testGetValidDouble() { String[][] testData = { // Source Expected Result // {null, "123"}, {"100.5", "100.5"}, {"0", "0"}, {"junk", "123"}, {"", "123"}, {"null", "123"} }; for (String[] aTestData : testData) { String source = aTestData[0]; Double expected = (aTestData[1] != null) ? new Double(aTestData[1]) : null; TestCase.assertEquals("Validating double '" + source + "'", expected, xssAPI.getValidDouble(source, 123)); } } @Test public void testGetValidDimension() { String[][] testData = { // Source Expected Result // {null, "123"}, {"", "123"}, {"100", "100"}, {"0", "0"}, {"junk", "123"}, {"100.5", "123"}, {"", "123"}, {"null", "123"}, {"\"auto\"", "\"auto\""}, {"'auto'", "\"auto\""}, {"auto", "\"auto\""}, {"autox", "123"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("Validating dimension '" + source + "'", expected, xssAPI.getValidDimension(source, "123")); } } @Test public void testEncodeForJSString() { String[][] testData = { // Source Expected Result // {null, null}, {"simple", "simple"}, {"break\"out", "break\\x22out"}, {"break'out", "break\\x27out"}, {"</script>", "<\\/script>"}, {"'alert(document.cookie)", "\\x27alert(document.cookie)"}, {"2014-04-22T10:11:24.002+01:00", "2014\\u002D04\\u002D22T10:11:24.002+01:00"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("Encoding '" + source + "'", expected, xssAPI.encodeForJSString(source)); } } @Test public void testGetValidJSToken() { String[][] testData = { // Source Expected Result // {null, RUBBISH}, {"", RUBBISH}, {"simple", "simple"}, {"clickstreamcloud.thingy", "clickstreamcloud.thingy"}, {"break out", RUBBISH}, {"break,out", RUBBISH}, {"\"literal string\"", "\"literal string\""}, {"'literal string'", "'literal string'"}, {"\"bad literal'", RUBBISH}, {"'literal'); junk'", "'literal\\x27); junk'"}, {"1200", "1200"}, {"3.14", "3.14"}, {"1,200", RUBBISH}, {"1200 + 1", RUBBISH} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; TestCase.assertEquals("Validating Javascript token '" + source + "'", expected, xssAPI.getValidJSToken(source, RUBBISH)); } } @Test public void testEncodeForCSSString() { String[][] testData = { // Source Expected result {null, null}, {"test" , "test"}, {"\\" , "\\5c"}, {"'" , "\\27"}, {"\"" , "\\22"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; String result = xssAPI.encodeForCSSString(source); TestCase.assertEquals("Encoding '" + source + "'", expected, result); } } @Test public void testGetValidStyleToken() { String[][] testData = { // Source Expected result {null , RUBBISH}, {"" , RUBBISH}, // CSS close {"}" , RUBBISH}, // line break {"br\neak" , RUBBISH}, // no javascript: {"javascript:alert(1)" , RUBBISH}, {"'javascript:alert(1)'" , RUBBISH}, {"\"javascript:alert('XSS')\"" , RUBBISH}, {"url(javascript:alert(1))" , RUBBISH}, {"url('javascript:alert(1)')" , RUBBISH}, {"url(\"javascript:alert('XSS')\")" , RUBBISH}, // no expression {"expression(alert(1))" , RUBBISH}, {"expression (alert(1))" , RUBBISH}, {"expression(this.location='a.co')" , RUBBISH}, // html tags {"</style><script>alert(1)</script>", RUBBISH}, // usual CSS stuff {"background-color" , "background-color"}, {"-moz-box-sizing" , "-moz-box-sizing"}, {".42%" , ".42%"}, {"#fff" , "#fff"}, // valid strings {"'literal string'" , "'literal string'"}, {"\"literal string\"" , "\"literal string\""}, {"'it\\'s here'" , "'it\\'s here'"}, {"\"it\\\"s here\"" , "\"it\\\"s here\""}, // invalid strings {"\"bad string" , RUBBISH}, {"'it's here'" , RUBBISH}, {"\"it\"s here\"" , RUBBISH}, // valid parenthesis {"rgb(255, 255, 255)" , "rgb(255, 255, 255)"}, // invalid parenthesis {"rgb(255, 255, 255" , RUBBISH}, {"255, 255, 255)" , RUBBISH}, // valid tokens {"url(http://example.com/test.png)", "url(http://example.com/test.png)"}, {"url('image/test.png')" , "url('image/test.png')"}, // invalid tokens {"color: red" , RUBBISH} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; String result = xssAPI.getValidStyleToken(source, RUBBISH); if (!result.equals(expected)) { fail("Validating style token '" + source + "', expecting '" + expected + "', but got '" + result + "'"); } } } @Test public void testGetValidCSSColor() { String[][] testData = { // Source Expected Result // {null, RUBBISH}, {"", RUBBISH}, {"rgb(0,+0,-0)", "rgb(0,+0,-0)"}, {"rgba ( 0\f%, 0%,\t0%,\n100%\r)", "rgba ( 0\f%, 0%,\t0%,\n100%\r)",}, {"#ddd", "#ddd"}, {"#eeeeee", "#eeeeee",}, {"hsl(0,1,2)", "hsl(0,1,2)"}, {"hsla(0,1,2,3)", "hsla(0,1,2,3)"}, {"currentColor", "currentColor"}, {"transparent", "transparent"}, {"\f\r\n\t MenuText\f\r\n\t ", "MenuText"}, {"expression(99,99,99)", RUBBISH}, {"blue;", RUBBISH}, {"url(99,99,99)", RUBBISH} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; String result = xssAPI.getValidCSSColor(source, RUBBISH); if (!result.equals(expected)) { fail("Validating CSS Color '" + source + "', expecting '" + expected + "', but got '" + result + "'"); } } } @Test public void testGetValidMultiLineComment() { String[][] testData = { //Source Expected Result {null , RUBBISH}, {"blah */ hack" , RUBBISH}, {"Valid comment" , "Valid comment"} }; for (String[] aTestData : testData) { String source = aTestData[0]; String expected = aTestData[1]; String result = xssAPI.getValidMultiLineComment(source, RUBBISH); if (!result.equals(expected)) { fail("Validating multiline comment '" + source + "', expecting '" + expected + "', but got '" + result + "'"); } } } @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 }, { "<?xml version=\"1.0\"?><!DOCTYPE test SYSTEM \"http://nonExistentHost:1234/\"><test/>", "<?xml version=\"1.0\"?><!DOCTYPE test SYSTEM \"http://nonExistentHost:1234/\"><test/>" } }; 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 + "'"); } } } }