This is an automated email from the ASF dual-hosted git repository. lprimak pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/shiro.git
commit e6c68162bf4fd12b8c62cd5a4afc5da0f39d6c9e Author: lprimak <[email protected]> AuthorDate: Wed Oct 21 16:15:44 2026 -0500 feat: add options to control forwarded-proto and port filter --- .../org/apache/shiro/ee/filters/ShiroFilter.java | 8 ++++-- .../ee/listeners/EnvironmentLoaderListener.java | 10 ++++++++ .../apache/shiro/web/filter/authz/PortFilter.java | 30 +++++++++++++++++++++- .../shiro/web/filter/authz/PortFilterTest.java | 27 +++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java index 733fbae74..c626d0e20 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java @@ -19,6 +19,7 @@ import static org.apache.shiro.ee.filters.FormResubmitSupport.isViewRebuildCandi import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.getCharacterEncoding; import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isCharEncodingEnabled; import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isShiroEEDisabled; +import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isXForwardedProtoDisabled; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; @@ -66,7 +67,9 @@ import org.omnifaces.util.Utils; * Thus, force null EJB principal for the web session, * as the real principal comes from the EjbSecurityFilter's doAs() call * <p/> - * Also handles X-Forwarded-Proto support + * Also handles X-Forwarded-Proto support, which can be turned off + * with the {@code org.apache.shiro.ee.x-forwarded-proto.disabled} context parameter. + * The header is trusted by default, so the proxy must overwrite or strip any client-supplied value */ @Slf4j @WebFilter(filterName = "ShiroFilter", urlPatterns = "/*", @@ -120,7 +123,8 @@ public class ShiroFilter extends org.apache.shiro.web.servlet.ShiroFilter { } private boolean createHttpButNeedHttps() { - return !HTTPS_SCHEME.equalsIgnoreCase(super.getScheme()) + return !isXForwardedProtoDisabled(servletContext) + && !HTTPS_SCHEME.equalsIgnoreCase(super.getScheme()) && HTTPS_SCHEME.equalsIgnoreCase(WebUtils.toHttp(getRequest()) .getHeader(X_FORWARDED_PROTO)); } diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java index 1296cd3f5..5370930e3 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java @@ -37,6 +37,7 @@ import static org.apache.shiro.ee.listeners.IniEnvironment.hasFacesContext; public class EnvironmentLoaderListener extends EnvironmentLoader implements ServletContextListener { private static final String SHIRO_EE_DISABLED_PARAM = "org.apache.shiro.ee.disabled"; private static final String SHIRO_EE_REDIRECT_DISABLED_PARAM = "org.apache.shiro.ee.redirect.disabled"; + private static final String SHIRO_EE_X_FORWARDED_PROTO_DISABLED_PARAM = "org.apache.shiro.ee.x-forwarded-proto.disabled"; private static final String SHIRO_EE_ENABLE_URL_SESSION_TRACKING_PARAM = "org.apache.shiro.ee.enable-url-session-tracking"; private static final String SHIRO_EE_SESSION_TRACKING_CONFIGURATION_DISABLED_PARAM = "org.apache.shiro.ee.session-tracking-configuration.disabled"; @@ -59,6 +60,14 @@ public class EnvironmentLoaderListener extends EnvironmentLoader implements Serv return Boolean.TRUE.equals(ctx.getAttribute(SHIRO_EE_REDIRECT_DISABLED_PARAM)); } + /** + * @param ctx servlet context + * @return whether honoring the {@code X-Forwarded-Proto} request header is disabled + */ + public static boolean isXForwardedProtoDisabled(ServletContext ctx) { + return Boolean.TRUE.equals(ctx.getAttribute(SHIRO_EE_X_FORWARDED_PROTO_DISABLED_PARAM)); + } + public static boolean isFormResubmitDisabled(ServletContext ctx) { return Boolean.TRUE.equals(ctx.getAttribute(FORM_RESUBMIT_DISABLED_PARAM)); } @@ -103,6 +112,7 @@ public class EnvironmentLoaderListener extends EnvironmentLoader implements Serv public void contextInitialized(ServletContextEvent sce) { copyBooleanInitParameter(sce.getServletContext(), SHIRO_EE_DISABLED_PARAM); copyBooleanInitParameter(sce.getServletContext(), SHIRO_EE_REDIRECT_DISABLED_PARAM); + copyBooleanInitParameter(sce.getServletContext(), SHIRO_EE_X_FORWARDED_PROTO_DISABLED_PARAM); copyBooleanInitParameter(sce.getServletContext(), FORM_RESUBMIT_DISABLED_PARAM); copyBooleanInitParameter(sce.getServletContext(), FORM_RESUBMIT_ANONYMOUS_DISABLED_PARAM); String secureCookiesStr = sce.getServletContext().getInitParameter(FORM_RESUBMIT_SECURE_COOKIES); diff --git a/web/src/main/java/org/apache/shiro/web/filter/authz/PortFilter.java b/web/src/main/java/org/apache/shiro/web/filter/authz/PortFilter.java index 20618c27e..bd0919380 100644 --- a/web/src/main/java/org/apache/shiro/web/filter/authz/PortFilter.java +++ b/web/src/main/java/org/apache/shiro/web/filter/authz/PortFilter.java @@ -40,6 +40,15 @@ import java.io.IOException; * # override for just this path: * /another/path/** = port[8080] * </pre> + * <p/> + * This filter is a redirect helper, not an access control. By default the port is taken from + * {@link ServletRequest#getServerPort()}, which most containers derive from the {@code Host} header + * (or from forwarded headers when so configured), so a client can influence it. Restrictions that must hold, + * such as limiting a path to an internal listener, belong in listener or network configuration and in + * authentication and authorization filters. + * <p/> + * Behind a reverse proxy or load balancer, leave {@link #setUseLocalPort(boolean) useLocalPort} off, since the + * container's local port is then not the port the client used. * * @since 1.0 */ @@ -50,6 +59,7 @@ public class PortFilter extends AuthorizationFilter { public static final String HTTP_SCHEME = "http"; private int port = DEFAULT_HTTP_PORT; + private boolean useLocalPort; public int getPort() { return port; @@ -59,6 +69,24 @@ public class PortFilter extends AuthorizationFilter { this.port = port; } + public boolean isUseLocalPort() { + return useLocalPort; + } + + /** + * Sets whether the access check compares the required port with {@link ServletRequest#getLocalPort()}, + * the port the container actually accepted the connection on, instead of + * {@link ServletRequest#getServerPort()}, which a client can influence through the {@code Host} header. + * Only enable this when clients connect directly to the container, not through a proxy that changes the port. + * The redirect issued on denial is not affected. Defaults to {@code false}. + * + * @param useLocalPort whether to compare against the local port + * @since 3.0.2 + */ + public void setUseLocalPort(boolean useLocalPort) { + this.useLocalPort = useLocalPort; + } + protected int toPort(Object mappedValue) { String[] ports = (String[]) mappedValue; if (ports == null || ports.length == 0) { @@ -74,7 +102,7 @@ public class PortFilter extends AuthorizationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { int requiredPort = toPort(mappedValue); - int requestPort = request.getServerPort(); + int requestPort = useLocalPort ? request.getLocalPort() : request.getServerPort(); return requiredPort == requestPort; } diff --git a/web/src/test/java/org/apache/shiro/web/filter/authz/PortFilterTest.java b/web/src/test/java/org/apache/shiro/web/filter/authz/PortFilterTest.java index 70cae5d19..d0588ea62 100644 --- a/web/src/test/java/org/apache/shiro/web/filter/authz/PortFilterTest.java +++ b/web/src/test/java/org/apache/shiro/web/filter/authz/PortFilterTest.java @@ -90,4 +90,31 @@ public class PortFilterTest { assertThat(result).isFalse(); } + @SuppressWarnings("checkstyle:MagicNumber") + @Test + void testServerPortUsedByDefault() throws Exception { + HttpServletRequest request = createNiceMock(HttpServletRequest.class); + expect(request.getServerPort()).andReturn(8443).anyTimes(); + expect(request.getLocalPort()).andReturn(8080).anyTimes(); + replay(request); + + PortFilter filter = new PortFilter(); + assertThat(filter.isAccessAllowed(request, null, new String[] {"8443"})).isTrue(); + assertThat(filter.isAccessAllowed(request, null, new String[] {"8080"})).isFalse(); + } + + @SuppressWarnings("checkstyle:MagicNumber") + @Test + void testLocalPortUsedWhenConfigured() throws Exception { + HttpServletRequest request = createNiceMock(HttpServletRequest.class); + expect(request.getServerPort()).andReturn(8443).anyTimes(); + expect(request.getLocalPort()).andReturn(8080).anyTimes(); + replay(request); + + PortFilter filter = new PortFilter(); + filter.setUseLocalPort(true); + assertThat(filter.isAccessAllowed(request, null, new String[] {"8080"})).isTrue(); + assertThat(filter.isAccessAllowed(request, null, new String[] {"8443"})).isFalse(); + } + }
