blob: e0a8ba4661e12836eaa42b9a9b2fe31e65f603f9 [file] [log] [blame]
/*******************************************************************************
* 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_");
}
});
} 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>", "&lt;script&gt;"},
{"<b>", "&lt;b&gt;"},
{"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>", "&lt;script>"},
{"\" <script>alert('pwned');</script>", "&#34; &lt;script>alert(&#39;pwned&#39;);&lt;/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>", "&lt;script&gt;"},
{"<b>", "&lt;b&gt;"},
{"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>", "&lt;script>"},
{"<b>", "&lt;b>"},
{"günter", "günter"},
{"\"xss:expression(alert('XSS'))", "&#34;xss:expression(alert(&#39;XSS&#39;))"}
};
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
//
{
"/base?backHref=%26%23x6a%3b%26%23x61%3b%26%23x76%3b%26%23x61%3b%26%23x73%3b%26%23x63%3b%26%23x72%3b%26%23x69%3b%26%23x70%3b%26%23x74%3b%26%23x3a%3balert%281%29",
"/base?backHref=javascript%3Aalert(1)"
},
{
"%26%23x6a%3b%26%23x61%3b%26%23x76%3b%26%23x61%3b%26%23x73%3b%26%23x63%3b%26%23x72%3b%26%23x69%3b%26%23x70%3b%26%23x74%3b%26%23x3a%3balert%281%29",
""
},
{
"&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;alert(1)",
""
},
{"%2Fscripts%2Ftest.js", "/scripts/test.js"},
{"/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 + "'");
}
}
}
}