/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
* #L%
*/
package org.alfresco.repo.web.scripts;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.webscripts.DeclarativeRegistry;
import org.springframework.extensions.webscripts.Description;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.Request;
import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
import org.springframework.extensions.webscripts.WebScript;
import org.springframework.extensions.webscripts.WebScriptException;
/**
* https://issues.alfresco.com/jira/browse/MNT-13180
*
* Customer would like to be sure that we protect ourselves against unsanitized user inputs that can lead to XSS vulnerabilities.
* This probably means (at least) implementing a Unit Test framework that injects into each webscript listed at:
* http://localhost:8080/alfresco/wcservice/index/uri/
* and for each documented parameter malicious input such as those used by our customer to detect the ones he found
*
* @author Viachaslau Tsikhanovich
*/
public class XssVulnerabilityTest extends BaseWebScriptTest
{
private Log logger = LogFactory.getLog(XssVulnerabilityTest.class);
private DeclarativeRegistry webscriptsRegistry;
static final String START_ARG = "{";
static final String END_ARG = "}";
static final String[] METHODS_TO_CHECK_ARRAY = { "GET", "DELETE", "POST", "PUT" };
static final Set METHODS_TO_CHECK_SET = new HashSet(Arrays.asList(METHODS_TO_CHECK_ARRAY));
static final String[] FORMATS_TO_CHECK_ARRAY = { "html" };
static final Set FORMATS_TO_CHECK_SET = new HashSet(Arrays.asList(FORMATS_TO_CHECK_ARRAY));
static final String[] URI_TO_SKIP_ARRAY = { ".rss", ".atom" }; // javascript is not executed for feeds
static final String MALARG1 = "";
static final String MALARG2 = "";
static final String MALARG3 = "\"";
static final String MALARG4 = "'\"";
static final String[] MALICIOUS_ARGS = { MALARG1, MALARG2, MALARG3, MALARG4 };
static final String[] SKIP_WEBSCRIPT_CHECK_ARRAY = {
"org/alfresco/cmis/client/cmisbrowser/federatedquery.get", /** argument is put into form's textarea but javascript is not executed **/
"org/alfresco/cmis/test.post.desc.xml"
};
static final Set SKIP_WEBSCRIPT_CHECK_ID_SET = new HashSet(Arrays.asList(SKIP_WEBSCRIPT_CHECK_ARRAY));
protected void setUp() throws Exception
{
super.setUp();
this.webscriptsRegistry = (DeclarativeRegistry)getServer().getApplicationContext().getBean("webscripts.registry.prototype");
setDefaultRunAs(AuthenticationUtil.getAdminUserName());
}
protected void tearDown() throws Exception
{
super.tearDown();
}
protected Log getLogger()
{
return logger;
}
public void testXssVulnerability() throws Throwable
{
webscriptsRegistry.reset();
final int scriptsSize = webscriptsRegistry.getWebScripts().size();
int i = 0, successCount = 0, wserrcount = 0, vulnCount = 0;
LinkedList vulnerabileURLS = new LinkedList();
for(WebScript ws : webscriptsRegistry.getWebScripts())
{
if (getLogger().isDebugEnabled())
{
getLogger().debug("progress: " + ++i + "/" + scriptsSize);
}
Description wsDesc = ws.getDescription();
if (SKIP_WEBSCRIPT_CHECK_ID_SET.contains(wsDesc.getId()))
{
// skip
continue;
}
boolean isMethodCheck = METHODS_TO_CHECK_SET.contains(wsDesc.getMethod());
boolean isFormatCheck = FORMATS_TO_CHECK_SET.contains(wsDesc.getDefaultFormat());
if (isMethodCheck && isFormatCheck)
{
for (String malArg : MALICIOUS_ARGS)
{
String[] uris = wsDesc.getURIs();
for (String uri : uris)
{
if (isUriSkip(uri))
{
continue;
}
// always parse url because we cannot rely on getArguments():
// - sometimes getArguments() returns null although URI has arguments
// - sometimes getArguments() returns set of args that does not contain args from url
List parsedArgs = parseArgsFromURI(uri);
if (0 == parsedArgs.size())
{
// no arguments in uri, skip
continue;
}
String url = substituteMaliciousArgInURI(uri, parsedArgs, malArg);
Response resp;
try
{
resp = sendRequest(createRequest(wsDesc.getMethod(), url), -1);
}
catch (WebScriptException e)
{
// skip webscript errors
++ wserrcount;
continue;
}
String respString = resp.getContentAsString();
if (resp.getStatus() == Status.STATUS_OK)
{
++successCount;
}
// do case insensitive check because argument can be converted to lowercase on page
if (respString.toLowerCase().contains(malArg.toLowerCase()))
{
vulnerabileURLS.add(wsDesc.getMethod() + " " + url);
vulnCount++;
}
}
}
}
}
if (getLogger().isDebugEnabled())
{
getLogger().debug("OK html responses count: " + successCount);
getLogger().debug("Webscript errors count: " + wserrcount);
getLogger().debug("Vulnerabile URLs count: " + vulnCount);
}
for (String url : vulnerabileURLS)
{
getLogger().warn("Vulnerabile URL: " + url);
}
assertTrue("Vulnerabile URLs found: " + vulnerabileURLS, vulnerabileURLS.size() == 0);
}
private boolean isUriSkip(String uri)
{
for (String uriPart : URI_TO_SKIP_ARRAY)
{
if (uri.contains(uriPart))
{
return true;
}
}
return false;
}
private List parseArgsFromURI(String uri)
{
List args = new LinkedList();
int startBracketInd = uri.indexOf(START_ARG, 0);
while (startBracketInd != -1)
{
int endBracketInd = uri.indexOf(END_ARG, startBracketInd);
if (endBracketInd != -1)
{
String arg = uri.substring(startBracketInd + 1, endBracketInd);
if (arg.endsWith("?"))
{
// optional argument
arg = arg.substring(0, arg.length() - 1);
}
args.add(arg);
// search next argument
startBracketInd = uri.indexOf(START_ARG, endBracketInd);
}
else
{
// no ending bracket
throw new AlfrescoRuntimeException("Invalid webscript URI : " + uri);
}
}
return args;
}
private Request createRequest(String method, String url) throws Exception
{
switch (method)
{
case "DELETE":
return new DeleteRequest(url);
case "GET":
return new GetRequest(url);
case "PUT":
return new PutRequest(url, "{}", "application/json");
case "POST":
return new PostRequest(url, "{}", "application/json");
default:
throw new InvalidArgumentException("HTTP method not supported");
}
}
private String substituteMaliciousArgInURI(String uri, List urlArgs, String malArg)
{
String url = uri;
// substitute malicious arguments
for (String arg : urlArgs)
{
url = url.replace(START_ARG + arg + END_ARG, "a" + malArg);
url = url.replace(START_ARG + arg + "?" + END_ARG, "a" + malArg);
}
if (url.contains(START_ARG) || url.contains(END_ARG))
{
throw new AlfrescoRuntimeException("Arguments were not properly substituted: " + url);
}
return url;
}
}