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 42aa34467ad30f085fc8e003042db0c397fe8d57 Author: lprimak <[email protected]> AuthorDate: Sat Jun 20 14:00:47 2026 -0500 bugfix(guice-web): now matching paths with trailing slash stripped --- .../shiro/guice/web/SimpleFilterChainResolver.java | 41 +++++++++------- .../shiro/guice/web/PathMatcherBypassTest.java | 55 ++++++++++++++++++++++ .../guice/web/SimpleFilterChainResolverTest.java | 8 ++-- .../mgt/PathMatchingFilterChainResolver.java | 22 +++++---- 4 files changed, 98 insertions(+), 28 deletions(-) diff --git a/support/guice/src/main/java/org/apache/shiro/guice/web/SimpleFilterChainResolver.java b/support/guice/src/main/java/org/apache/shiro/guice/web/SimpleFilterChainResolver.java index bca3a4c79..3981f4db6 100644 --- a/support/guice/src/main/java/org/apache/shiro/guice/web/SimpleFilterChainResolver.java +++ b/support/guice/src/main/java/org/apache/shiro/guice/web/SimpleFilterChainResolver.java @@ -33,6 +33,7 @@ import com.google.inject.Key; import org.apache.shiro.util.PatternMatcher; import org.apache.shiro.web.filter.mgt.FilterChainResolver; import org.apache.shiro.web.util.WebUtils; +import static org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver.removeTrailingSlash; class SimpleFilterChainResolver implements FilterChainResolver { private final Map<String, Key<? extends Filter>[]> chains; @@ -46,26 +47,34 @@ class SimpleFilterChainResolver implements FilterChainResolver { } public FilterChain getChain(ServletRequest request, ServletResponse response, final FilterChain originalChain) { - String path = WebUtils.getPathWithinApplication(WebUtils.toHttp(request)); - for (final String pathPattern : chains.keySet()) { - if (patternMatcher.matches(pathPattern, path)) { - final Iterator<Key<? extends Filter>> chain = Arrays.asList(chains.get(pathPattern)).iterator(); - return new SimpleFilterChain(originalChain, new Iterator<Filter>() { - public boolean hasNext() { - return chain.hasNext(); - } + String requestURI = WebUtils.getPathWithinApplication(WebUtils.toHttp(request)); + final String requestURINoTrailingSlash = removeTrailingSlash(requestURI); - public Filter next() { - return injector.getInstance(chain.next()); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }); + for (String pathPattern : chains.keySet()) { + if (patternMatcher.matches(pathPattern, requestURI)) { + return proxy(originalChain, pathPattern); + } else { + pathPattern = removeTrailingSlash(pathPattern); + if (patternMatcher.matches(pathPattern, requestURINoTrailingSlash)) { + return proxy(originalChain, pathPattern); + } } } return null; } + private FilterChain proxy(FilterChain originalChain, String pathPattern) { + final Iterator<Key<? extends Filter>> chain = Arrays.asList(chains.get(pathPattern)).iterator(); + return new SimpleFilterChain(originalChain, new Iterator<>() { + public boolean hasNext() { + return chain.hasNext(); + } + public Filter next() { + return injector.getInstance(chain.next()); + } + public void remove() { + throw new UnsupportedOperationException(); + } + }); + } } diff --git a/support/guice/src/test/java/org/apache/shiro/guice/web/PathMatcherBypassTest.java b/support/guice/src/test/java/org/apache/shiro/guice/web/PathMatcherBypassTest.java new file mode 100644 index 000000000..8b7ff38cb --- /dev/null +++ b/support/guice/src/test/java/org/apache/shiro/guice/web/PathMatcherBypassTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.shiro.guice.web; + +import com.google.inject.Injector; +import com.google.inject.Key; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.shiro.util.AntPathMatcher; +import org.junit.jupiter.api.Test; +import java.util.LinkedHashMap; +import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PathMatcherBypassTest { + @Test + void guicePathBypass() { + AntPathMatcher matcher = new AntPathMatcher(); + @SuppressWarnings("unchecked") + Key<? extends Filter>[] adminChain = (Key<? extends Filter>[]) new Key<?>[]{ Key.get(Filter.class) }; + Map<String, Key<? extends Filter>[]> chains = new LinkedHashMap<>(); + chains.put("/admin/foo", adminChain); + + Injector injector = mock(Injector.class); + var resolver = new SimpleFilterChainResolver(chains, injector, matcher); + FilterChain orig = mock(FilterChain.class); + + // Case 2: trailing slash bypass + HttpServletRequest req2 = mock(HttpServletRequest.class); + when(req2.getServletPath()).thenReturn(""); + when(req2.getPathInfo()).thenReturn("/admin/foo/"); + FilterChain r2 = resolver.getChain(req2, mock(HttpServletResponse.class), orig); + assertThat(r2).isNotNull(); + } +} diff --git a/support/guice/src/test/java/org/apache/shiro/guice/web/SimpleFilterChainResolverTest.java b/support/guice/src/test/java/org/apache/shiro/guice/web/SimpleFilterChainResolverTest.java index e5768f793..24e6d9ee1 100644 --- a/support/guice/src/test/java/org/apache/shiro/guice/web/SimpleFilterChainResolverTest.java +++ b/support/guice/src/test/java/org/apache/shiro/guice/web/SimpleFilterChainResolverTest.java @@ -87,7 +87,7 @@ public class SimpleFilterChainResolverTest { expect(request.getCharacterEncoding()).andStubReturn(null); - expect(patternMatcher.matches(chainOne, "/mychain")).andReturn(false); + expect(patternMatcher.matches(chainOne, "/mychain")).andReturn(false).times(2); expect(patternMatcher.matches(chainTwo, "/mychain")).andReturn(true); Filter filter2a = ctrl.createMock(Filter.class); @@ -118,9 +118,9 @@ public class SimpleFilterChainResolverTest { expect(request.getCharacterEncoding()).andStubReturn(null); - expect(patternMatcher.matches(chainOne, "/nochain")).andReturn(false); - expect(patternMatcher.matches(chainTwo, "/nochain")).andReturn(false); - expect(patternMatcher.matches(chainThree, "/nochain")).andReturn(false); + expect(patternMatcher.matches(chainOne, "/nochain")).andReturn(false).times(2); + expect(patternMatcher.matches(chainTwo, "/nochain")).andReturn(false).times(2); + expect(patternMatcher.matches(chainThree, "/nochain")).andReturn(false).times(2); ctrl.replay(); diff --git a/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java b/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java index 4a6fbe10a..dda7f4575 100644 --- a/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java +++ b/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java @@ -153,6 +153,20 @@ public class PathMatchingFilterChainResolver implements FilterChainResolver { return null; } + /** + * Removes the trailing slash from the given path if it exists and is not the root path ("/"). + + * @param path input + * @return stripped path + */ + public static String removeTrailingSlash(String path) { + if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path) + && path.endsWith(DEFAULT_PATH_SEPARATOR)) { + return path.substring(0, path.length() - 1); + } + return path; + } + /** * Returns {@code true} if an incoming request path (the {@code path} argument) * matches a configured filter chain path (the {@code pattern} argument), {@code false} otherwise. @@ -186,12 +200,4 @@ public class PathMatchingFilterChainResolver implements FilterChainResolver { protected String getPathWithinApplication(ServletRequest request) { return WebUtils.getPathWithinApplication(WebUtils.toHttp(request)); } - - private static String removeTrailingSlash(String path) { - if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path) - && path.endsWith(DEFAULT_PATH_SEPARATOR)) { - return path.substring(0, path.length() - 1); - } - return path; - } }
