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();
+    }
+
 }

Reply via email to