Skip to content

Commit

Permalink
[GEOS-11289] Enable Spring Security StrictHttpFirewall by default
Browse files Browse the repository at this point in the history
  • Loading branch information
sikeoka authored and aaime committed Feb 7, 2024
1 parent 829e937 commit 77afad5
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 4 deletions.
14 changes: 12 additions & 2 deletions doc/en/user/source/installation/upgrade.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ The general GeoServer upgrade process is as follows:
Notes on upgrading specific versions
------------------------------------

WCS ArcGRID output format removal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Spring Security Strict HTTP Firewall (GeoServer 2.25 and newer)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

As of GeoServer 2.25, Spring Security's StrictHttpFirewall will be enabled by default which will provide stronger
default protection, particularly against potential path traversal vulnerabilities.

In some cases valid requests may be blocked if the names of GeoServer resources (e.g., workspaces) contain certain
special characters and are included in URL paths. See the :ref:`production_config_spring_firewall` page for
instructions to disable the strict firewall and revert to the DefaultHttpFirewall used by earlier versions.

WCS ArcGRID output format removal (GeoServer 2.24 and newer)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The ArcGRID output format for WCS has been removed in GeoServer 2.24.0.
If you have been using this format, you will need to switch to another text based format,
Expand Down
11 changes: 11 additions & 0 deletions doc/en/user/source/production/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,17 @@ GeoServer provides a number of facilities to control external entity resolution:

This setting prevents ``ENTITY_RESOLUTION_ALLOWLIST`` from being used.

.. _production_config_spring_firewall:

Spring Security Firewall
------------------------

GeoServer defaults to using Spring Security's StrictHttpFirewall to help improve protection against potentially malicious
requests. However, some users will need to disable the StrictHttpFirewall if the names of GeoServer resources (workspaces,
layers, styles, etc.) in URL paths need to contain encoded percent, encoded period or decoded or encoded semicolon characters.
The ``GEOSERVER_USE_STRICT_FIREWALL`` property can be set to false either via Java system property, command line argument
(-D), environment variable or web.xml init parameter to use the more lenient DefaultHttpFirewall.

Session Management
------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* (c) 2024 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.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.geoserver.platform.GeoServerExtensions;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;

/**
* A custom {@link HttpFirewall} implementation that allows GeoServer administrators to enable or
* disable the use of the stricter {@link StrictHttpFirewall} through a system property. The {@link
* DefaultHttpFirewall} provides weaker protections but may be necessary for some users who need to
* use certain special characters in URL paths.
*/
public class GeoServerHttpFirewall implements HttpFirewall {

/**
* System property to control whether or not to run requests through {@link StrictHttpFirewall}.
* When set to false, requests will only be run through {@link DefaultHttpFirewall} which is
* more lenient but also more likely to allow malicious requests. Default is true.
*/
public static final String USE_STRICT_FIREWALL = "GEOSERVER_USE_STRICT_FIREWALL";

private final DefaultHttpFirewall defaultFirewall = new DefaultHttpFirewall();

private final StrictHttpFirewall strictFirewall = new StrictHttpFirewall();

@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) {
// run a modified request with normalized URL paths through Spring Security's
// StrictHttpFirewall but do not forward the normalized request to the proxy
if (!"false".equalsIgnoreCase(GeoServerExtensions.getProperty(USE_STRICT_FIREWALL))) {
this.strictFirewall.getFirewalledRequest(new NormalizedHttpServletRequest(request));
}
// use DefaultHttpFirewall here with the original request
return this.defaultFirewall.getFirewalledRequest(request);
}

@Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
// strict and default firewalls have identical behavior here
return this.defaultFirewall.getFirewalledResponse(response);
}

/**
* An {@link HttpServletRequestWrapper} that allows running a {@link HttpServletRequest} through
* {@link StrictHttpFirewall} even when the URL path contains two consecutive slashes since
* there are use cases where GeoServer needs to allow this type of non-normalized URL path.
*/
private static class NormalizedHttpServletRequest extends HttpServletRequestWrapper {

/** Regular expression for two or more consecutive forward slashes. */
private static final Pattern FORWARD_SLASHES = Pattern.compile("//+");

private NormalizedHttpServletRequest(HttpServletRequest request) {
super(request);
}

/** Replaces consecutive forward slashes with a single slash. */
private static String normalizeSlashes(String path) {
return path != null ? FORWARD_SLASHES.matcher(path).replaceAll("/") : null;
}

@Override
public String getContextPath() {
return normalizeSlashes(super.getContextPath());
}

@Override
public String getPathInfo() {
return normalizeSlashes(super.getPathInfo());
}

@Override
public String getRequestURI() {
return normalizeSlashes(super.getRequestURI());
}

@Override
public String getServletPath() {
return normalizeSlashes(super.getServletPath());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.util.matcher.RequestMatcher;

public class GeoServerSecurityFilterChainProxy
Expand Down Expand Up @@ -205,7 +204,7 @@ void createFilterChain() {
securityManager.getAuthenticationCache().removeAll();

proxy = new FilterChainProxy(filterChains);
proxy.setFirewall(new DefaultHttpFirewall());
proxy.setFirewall(new GeoServerHttpFirewall());
proxy.afterPropertiesSet();
chainsInitialized = true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* (c) 2024 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.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;

import org.junit.After;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.web.firewall.RequestRejectedException;

public class GeoServerHttpFirewallTest {

private final GeoServerHttpFirewall firewall = new GeoServerHttpFirewall();

@After
public void resetProperty() {
setProperty(null);
}

@Test
public void testForwardSlashEncoded() {
// decoded forward slashes are not blocked
// this is blocked by both strict and default firewalls
// some servers may block this even if this firewall wasn't blocking it
doTestPath(null, "%2f", true);
doTestPath(true, "%2f", true);
doTestPath(false, "%2f", true);
doTestPath(null, "%2F", true);
doTestPath(true, "%2F", true);
doTestPath(false, "%2F", true);
}

@Test
public void testBackSlashDecoded() {
// this is actually an invalid URL that servers will probably block
doTestPath(null, "\\", true);
doTestPath(true, "\\", true);
doTestPath(false, "\\", false);
}

@Test
public void testBackSlashEncoded() {
// some servers may block this even when the strict firewall is disabled
doTestPath(null, "%5c", true);
doTestPath(true, "%5c", true);
doTestPath(false, "%5c", false);
doTestPath(null, "%5C", true);
doTestPath(true, "%5C", true);
doTestPath(false, "%5C", false);
}

@Test
public void testPercentEncoded() {
// decoded percents create invalid URLs that don't work
doTestPath(null, "%25", true);
doTestPath(true, "%25", true);
doTestPath(false, "%25", false);
}

@Test
public void testPeriodEncoded() {
// decoded periods are not blocked
doTestPath(null, "%2e", true);
doTestPath(true, "%2e", true);
doTestPath(false, "%2e", false);
doTestPath(null, "%2E", true);
doTestPath(true, "%2E", true);
doTestPath(false, "%2E", false);
}

@Test
public void testSemicolonDecoded() {
doTestPath(null, ";", true);
doTestPath(true, ";", true);
doTestPath(false, ";", false);
}

@Test
public void testSemicolonEncoded() {
doTestPath(null, "%3b", true);
doTestPath(true, "%3b", true);
doTestPath(false, "%3b", false);
doTestPath(null, "%3B", true);
doTestPath(true, "%3B", true);
doTestPath(false, "%3B", false);
}

@Test
public void testNonNormalizedPath() {
// GeoServer needs this to be allowed
doTestPath(null, "//", false);
doTestPath(true, "//", false);
doTestPath(false, "//", false);
}

private void doTestPath(Boolean strict, String path, boolean exceptionExpected) {
setProperty(strict);
MockHttpServletRequest request = new MockHttpServletRequest("GET", path);
if (exceptionExpected) {
assertThrows(
RequestRejectedException.class,
() -> this.firewall.getFirewalledRequest(request));
} else {
assertNotNull(this.firewall.getFirewalledRequest(request));
}
}

private static void setProperty(Boolean strict) {
if (strict == null) {
System.clearProperty(GeoServerHttpFirewall.USE_STRICT_FIREWALL);
} else {
System.setProperty(GeoServerHttpFirewall.USE_STRICT_FIREWALL, strict.toString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@ protected MockHttpServletRequest createRequest(String path) {
protected MockHttpServletRequest createRequest(String path, boolean createSession) {
MockHttpServletRequest request = new GeoServerMockHttpServletRequest();

request.setMethod("GET");
request.setScheme("http");
request.setServerName("localhost");
request.setServerPort(8080);
Expand Down

0 comments on commit 77afad5

Please sign in to comment.