/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.security; import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import javax.servlet.Filter; import org.apache.commons.codec.binary.Base64; import org.geoserver.data.test.SystemTestData; import org.geoserver.security.config.BruteForcePreventionConfig; import org.geoserver.security.config.SecurityManagerConfig; import org.geoserver.test.GeoServerSystemTestSupport; import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; public class BruteForceAttackTest extends GeoServerSystemTestSupport { private static final String HELLO_GET_REQUEST = "ows?service=hello&request=hello&message=Hello%20World"; @Override protected void setUpSpring(List<String> springContextLocations) { super.setUpSpring(springContextLocations); // add the hello world services, in order to have a legit service to hit springContextLocations.add("classpath*:/org/geoserver/ows/applicationContext.xml"); } @Override protected void setUpTestData(SystemTestData testData) throws Exception { // only setup security, no data needed testData.setUpSecurity(); } @Override protected List<Filter> getFilters() { // enable security return Arrays.asList((javax.servlet.Filter) applicationContext .getBean(GeoServerSecurityFilterChainProxy.class)); } @Before public void resetAuthentication() { setRequestAuth(null, null); } @Before public void resetBruteForceAttackConfig() throws Exception { GeoServerSecurityManager manager = applicationContext .getBean(GeoServerSecurityManager.class); final SecurityManagerConfig securityConfig = manager.getSecurityConfig(); BruteForcePreventionConfig bruteForceConfig = securityConfig.getBruteForcePrevention(); bruteForceConfig.setEnabled(true); // one second fixed delay bruteForceConfig.setMinDelaySeconds(1); bruteForceConfig.setMaxDelaySeconds(1); bruteForceConfig.setMaxBlockedThreads(100); bruteForceConfig.setWhitelistedMasks(Collections.emptyList()); manager.saveSecurityConfig(securityConfig); } @Test public void testLoginDelay() throws Exception { // successful login, no wait (cannot actually test it) setRequestAuth("admin", "geoserver"); assertEquals(200, getAsServletResponse(HELLO_GET_REQUEST).getStatus()); // failing login, at least one second wait setRequestAuth("admin", "foobar"); long start = System.currentTimeMillis(); assertEquals(401, getAsServletResponse(HELLO_GET_REQUEST).getStatus()); long end = System.currentTimeMillis(); assertThat((end - start), greaterThan(1000l)); } @Test public void testParallelLogin() throws Exception { testParallelLogin("Concurrent login attempts during delay period not allowed", i -> "foo"); } @Test public void testTooManyBlockedThreads() throws Exception { // configure it to allow only one thread in the wait list GeoServerSecurityManager manager = applicationContext .getBean(GeoServerSecurityManager.class); final SecurityManagerConfig securityConfig = manager.getSecurityConfig(); BruteForcePreventionConfig bruteForceConfig = securityConfig.getBruteForcePrevention(); bruteForceConfig.setMaxBlockedThreads(1); manager.saveSecurityConfig(securityConfig); // hit with many different users testParallelLogin("Too many failed logins waiting on delay", i -> "foo" + i); } private void testParallelLogin(String expectedMessage, Function<Integer, String> userNameGenerator) throws InterruptedException, ExecutionException { // idea, setup several threads to do the same failing auth in parallel, // ensuring they are all ready to go at the same time using a latch final int NTHREADS = 32; ExecutorService service = Executors.newFixedThreadPool(NTHREADS); CountDownLatch latch = new CountDownLatch(NTHREADS); AtomicInteger concurrentLoginsPrevented = new AtomicInteger(0); List<Future<?>> futures = new ArrayList<>(); long start = System.currentTimeMillis(); for(int i = 0; i < NTHREADS; i++) { final int idx = i; Future<?> future = service.submit(() -> { // mark and ready and wait for others latch.countDown(); latch.await(); // execute request timing how long it took MockHttpServletRequest request = createRequest(HELLO_GET_REQUEST); request.setMethod( "GET" ); request.setContent(new byte[]{}); String userName = userNameGenerator.apply(idx); String token = userName + ":foobar"; request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); MockHttpServletResponse response = dispatch( request, "UTF-8" ); // check the response and see the error message assertEquals(401, response.getStatus()); final String message = response.getErrorMessage(); // System.out.println(message); if (message.contains(expectedMessage)) { concurrentLoginsPrevented.incrementAndGet(); } return null; }); futures.add(future); } // wait for termination for (Future<?> future : futures) { future.get(); } long awaitTime = System.currentTimeMillis() - start; service.shutdown(); // now, either the threads all serialized and waited (extremely unlikely, but // not impossible) or at least one got bumped immediately with a concurrent login message assertTrue(awaitTime > NTHREADS * 1000 || concurrentLoginsPrevented.get() > 0); } // "Too many failed logins waiting on delay already"; }