/* * #%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; } }