/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This 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 2.1 of * the License, or (at your option) any later version. * * This software 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 this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.csrf; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Random; import java.security.SecureRandom; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.junit.Assert; import org.jmock.Expectations; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.xwiki.bridge.DocumentAccessBridge; import org.xwiki.container.Container; import org.xwiki.container.servlet.ServletRequest; import org.xwiki.csrf.internal.DefaultCSRFToken; import org.xwiki.model.reference.DocumentReference; import org.xwiki.test.jmock.AbstractMockingComponentTestCase; import org.xwiki.test.jmock.annotation.MockingRequirement; import static org.hamcrest.Matchers.*; /** * Tests for the {@link DefaultCSRFToken} component. * * @version $Id: e68ecd3690b58919318bd1186b777dc773074121 $ * @since 2.5M2 */ @MockingRequirement(DefaultCSRFTokenTest.InsecureCSRFToken.class) public class DefaultCSRFTokenTest extends AbstractMockingComponentTestCase { /** URL of the current document. */ private static final String mockDocumentUrl = "http://host/xwiki/bin/save/Main/Test"; /** Resubmission URL. */ private static final String resubmitUrl = mockDocumentUrl; /** Tested CSRF token component. */ private CSRFToken csrf; /** * This class is here because it doesn't require a SecureRandom generator * seed on each startup. Seeding a SecureRandom generator can take a very long time, * especially many time over which depleats the random pool on the server. */ public static class InsecureCSRFToken extends DefaultCSRFToken { @Override public void initialize() { final Random random = new Random(System.nanoTime()); this.setRandom(new SecureRandom() { private static final long serialVersionUID = 3; @Override public void nextBytes(byte[] out) { random.nextBytes(out); } }); } } @Before public void configure() throws Exception { // set up mocked dependencies // document access bridge final DocumentAccessBridge mockDocumentAccessBridge = getComponentManager().getInstance(DocumentAccessBridge.class); final CopyStringMatcher returnValue = new CopyStringMatcher(resubmitUrl + "?", ""); getMockery().checking(new Expectations() { { allowing(mockDocumentAccessBridge).getDocumentURL(with(aNonNull(DocumentReference.class)), with("view"), with(returnValue), with(aNull(String.class))); will(returnValue); allowing(mockDocumentAccessBridge).getDocumentURL(with(aNull(DocumentReference.class)), with("view"), with(aNull(String.class)), with(aNull(String.class))); will(returnValue(mockDocumentUrl)); allowing(mockDocumentAccessBridge).getCurrentDocumentReference(); will(returnValue(null)); } }); // configuration final CSRFTokenConfiguration mockConfiguration = getComponentManager().getInstance(CSRFTokenConfiguration.class); getMockery().checking(new Expectations() { { allowing(mockConfiguration).isEnabled(); will(returnValue(true)); } }); // request final HttpSession mockSession = getMockery().mock(HttpSession.class); final HttpServletRequest httpRequest = getMockery().mock(HttpServletRequest.class); final ServletRequest servletRequest = new ServletRequest(httpRequest); getMockery().checking(new Expectations() { { allowing(httpRequest).getRequestURL(); will(returnValue(new StringBuffer(mockDocumentUrl))); allowing(httpRequest).getRequestURI(); will(returnValue(mockDocumentUrl)); allowing(httpRequest).getParameterMap(); will(returnValue(new HashMap<String, String[]>())); allowing(httpRequest).getSession(); will(returnValue(mockSession)); } }); // session getMockery().checking(new Expectations() { { allowing(mockSession).getAttribute(with(any(String.class))); will(returnValue(new HashMap<String, Object>())); } }); // container final Container mockContainer = getComponentManager().getInstance(Container.class); getMockery().checking(new Expectations() { { allowing(mockContainer).getRequest(); will(returnValue(servletRequest)); } }); // logging getMockery().checking(new Expectations() {{ // Ignore all calls to debug() ignoring(any(Logger.class)).method("debug"); }}); this.csrf = getComponentManager().getInstance(CSRFToken.class); } /** * Add a mocking role to have a logged user. * @throws Exception if problems occur */ private void userIsLogged() throws Exception { // document access bridge final DocumentAccessBridge mockDocumentAccessBridge = getComponentManager().getInstance(DocumentAccessBridge.class); getMockery().checking(new Expectations() { { allowing(mockDocumentAccessBridge).getCurrentUserReference(); will(returnValue(new DocumentReference("mainWiki", "XWiki", "Admin"))); } }); } /** * Test that the secret token is a non-empty string. */ @Test public void testToken() throws Exception { userIsLogged(); String token = this.csrf.getToken(); Assert.assertNotNull("CSRF token is null", token); Assert.assertNotSame("CSRF token is empty string", "", token); Assert.assertTrue("CSRF token is too short: \"" + token + "\"", token.length() > 20); } /** * Test that the secret token is a non-empty string, even for guest user. */ @Test public void testTokenForGuestUser() throws Exception { // document access bridge final DocumentAccessBridge mockDocumentAccessBridge = getComponentManager().getInstance(DocumentAccessBridge.class); getMockery().checking(new Expectations() { { allowing(mockDocumentAccessBridge).getCurrentUserReference(); will(returnValue(null)); } }); String token = this.csrf.getToken(); Assert.assertNotNull("CSRF token is null", token); Assert.assertNotSame("CSRF token is empty string", "", token); Assert.assertTrue("CSRF token is too short: \"" + token + "\"", token.length() > 20); } /** * Test that the same secret token is returned on subsequent calls. */ @Test public void testTokenTwice() throws Exception { userIsLogged(); String token1 = this.csrf.getToken(); String token2 = this.csrf.getToken(); Assert.assertNotNull("CSRF token is null", token1); Assert.assertNotSame("CSRF token is empty string", "", token1); Assert.assertEquals("Subsequent calls returned different tokens", token1, token2); } /** * Test that the produced valid secret token is indeed valid. */ @Test public void testTokenValidity() throws Exception { userIsLogged(); String token = this.csrf.getToken(); Assert.assertTrue("Valid token did not pass the check", this.csrf.isTokenValid(token)); } /** * Test that null is not valid. */ @Test public void testNullNotValid() throws Exception { userIsLogged(); // Verify that the correct message is logged final Logger logger = getMockLogger(); getMockery().checking(new Expectations() {{ oneOf(logger).warn(with(startsWith("CSRFToken: Secret token verification failed, token: \"null\", stored " + "token:"))); }}); Assert.assertFalse("Null passed validity check", this.csrf.isTokenValid(null)); } /** * Test that empty string is not valid. */ @Test public void testEmptyNotValid() throws Exception { userIsLogged(); // Verify that the correct message is logged final Logger logger = getMockLogger(); getMockery().checking(new Expectations() {{ oneOf(logger).warn(with(startsWith("CSRFToken: Secret token verification failed, token: \"\", stored " + "token:"))); }}); Assert.assertFalse("Empty string passed validity check", this.csrf.isTokenValid("")); } /** * Test that the prefix of the valid token is not valid. */ @Test public void testPrefixNotValid() throws Exception { userIsLogged(); // Verify that the correct message is logged final Logger logger = getMockLogger(); getMockery().checking(new Expectations() {{ oneOf(logger).warn(with(startsWith("CSRFToken: Secret token verification failed, token:"))); }}); String token = this.csrf.getToken(); if (token != null) { token = token.substring(0, token.length() - 2); } Assert.assertFalse("Null passed validity check", this.csrf.isTokenValid(token)); } /** * Test that the resubmission URL is correct. */ @Test public void testResubmissionURL() throws Exception { userIsLogged(); String url = this.csrf.getResubmissionURL(); try { // srid is random, extract it from the url Matcher matcher = Pattern.compile(".*srid%3D([a-zA-Z0-9]+).*").matcher(url); String srid = matcher.matches() ? matcher.group(1) : "asdf"; String resubmit = URLEncoder.encode(mockDocumentUrl + "?srid=" + srid, "utf-8"); String back = URLEncoder.encode(mockDocumentUrl, "utf-8"); String expected = resubmitUrl + "?resubmit=" + resubmit + "&xback=" + back + "&xpage=resubmit"; Assert.assertEquals("Invalid resubmission URL", expected, url); } catch (UnsupportedEncodingException exception) { Assert.fail("Should not happen: " + exception.getMessage()); } } /** * Tests if the token contains any special characters that have a potential to break the layout when used in places * where XWiki-syntax is allowed. */ @Test public void testXWikiSyntaxCompatibility() throws Exception { userIsLogged(); // We cannot easily control the value of the token, so we just test if it contains any "bad" characters and hope // for the best. Since the probability that the token contains some specific character is about 1/3, this test // will start to flicker (instead of always failing) if something like XWIKI-5996 is reintroduced for (int i = 0; i < 30; ++i) { this.csrf.clearToken(); String token = this.csrf.getToken(); Assert.assertFalse("The token \"" + token + "\" contains a character that might break the layout", token.matches(".*[&?*_/#^,.({\\[\\]})~!=+-].*")); } } }