/* (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 java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import org.geoserver.config.impl.GeoServerLifecycleHandler; import org.geoserver.security.config.BruteForcePreventionConfig; import org.geotools.util.logging.Logging; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AbstractAuthenticationEvent; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; /** * Brute force attack preventer * * @author Andrea Aime - GeoSolutions */ public class BruteForceListener implements ApplicationListener<AbstractAuthenticationEvent>, GeoServerLifecycleHandler { static final Logger LOGGER = Logging.getLogger(BruteForceListener.class); /** * Simple single node delayed login tracker. Should be made pluggable to allow by some sort of network service for a clustered installation */ Map<String, AtomicInteger> delayedUsers = new ConcurrentHashMap<>(); private GeoServerSecurityManager securityManager; public BruteForceListener(GeoServerSecurityManager securityManager) { this.securityManager = securityManager; } private BruteForcePreventionConfig getConfig() { BruteForcePreventionConfig config = securityManager.getSecurityConfig() .getBruteForcePrevention(); if (config == null) { return BruteForcePreventionConfig.DEFAULT; } else { return config; } } @Override public void onApplicationEvent(AbstractAuthenticationEvent event) { // is it enabled? BruteForcePreventionConfig config = getConfig(); if (!config.isEnabled()) { return; } // some addresses can be whitelisted and allowed to login anyways HttpServletRequest request = GeoServerSecurityFilterChainProxy.REQUEST.get(); if (requestAddressInWhiteList(request, config)) { return; } // Yes, enabled, check for concurrent login attempt Authentication authentication = event.getAuthentication(); String name = getUserName(authentication); if (name == null) { LOGGER.warning("Brute force attack prevention enabled, but Spring Authentication " + "does not provide a user name, skipping: " + authentication); } // do we have a delayed login in flight already? If so, kill this login attempt // no matter if successful or not final AtomicInteger counter = delayedUsers.get(name); if (counter != null) { int count = counter.incrementAndGet(); logFailedRequest(request, name, count); throw new ConcurrentAuthenticationException(name, count); } if (event instanceof AuthenticationFailureBadCredentialsEvent || event instanceof AuthenticationFailureProviderNotFoundEvent) { // are we above the max number of blocked threads already? int maxBlockedThreads = config.getMaxBlockedThreads(); if(maxBlockedThreads > 0 && delayedUsers.size() > maxBlockedThreads) { throw new MaxBlockedThreadsException(1); } delayedUsers.put(name, new AtomicInteger(1)); try { logFailedRequest(request, name, 0); long delay = computeDelay(config); if (delay > 0) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("Brute force attack prevention, delaying login for " + delay + "ms"); } Thread.sleep(delay); } } catch (InterruptedException e) { // duh } finally { delayedUsers.remove(name); } } } private boolean requestAddressInWhiteList(HttpServletRequest request, BruteForcePreventionConfig config) { // is there a white list? if (config.getWhitelistAddressMatchers() == null) { return false; } return config.getWhitelistAddressMatchers().stream() .anyMatch(matcher -> matcher.matches(request)); } private long computeDelay(BruteForcePreventionConfig config) { long min = config.getMinDelaySeconds() * 1000; long max = config.getMaxDelaySeconds() * 1000; return min + (long) ((max - min) * Math.random()); } /** * Returns the username for this authentication, or null if missing or cannot be determined * * @param authentication * @return */ private String getUserName(Authentication authentication) { if (authentication == null) { return null; } Object principal = authentication.getPrincipal(); if (principal != null) { if (principal instanceof UserDetails) { return ((UserDetails) principal).getUsername(); } else if (principal instanceof String) { return (String) principal; } } return authentication.getName(); } private void logFailedRequest(HttpServletRequest request, String name, int count) { StringBuilder sb = new StringBuilder("Failed login, user ").append(name).append(" from "); sb.append(request.getRemoteAddr()); // log x-forwarded-for too, but not exclusively as it can be spoofed String forwardedFor = request.getHeader("X-Forwarded-For"); if (forwardedFor != null) { sb.append(", forwarded for ").append(forwardedFor); } if (count > 0) { sb.append(", stopped ").append(count) .append(" concurrent logins during authentication delay"); } LOGGER.warning(sb.toString()); } @Override public void onReset() { delayedUsers.clear(); } @Override public void onDispose() { // nothing to do } @Override public void beforeReload() { // nothing to do } @Override public void onReload() { delayedUsers.clear(); } }