Apache Shiro: Auth bypass when accessing static files only on case-insensitive filesystems
Description
Authentication Bypass by Alternate Name vulnerability in Apache Shiro.
This issue affects Apache Shiro: before 2.0.7.
Users are recommended to upgrade to version 2.0.7, which fixes the issue.
The issue only effects static files. If static files are served from a case-insensitive filesystem, such as default macOS setup, static files may be accessed by varying the case of the filename in the request. If only lower-case (common default) filters are present in Shiro, they may be bypassed this way.
Shiro 2.0.7 and later has a new parameters to remediate this issue shiro.ini: filterChainResolver.caseInsensitive = true application.propertie: shiro.caseInsensitive=true
Shiro 3.0.0 and later (upcoming) makes this the default.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Shiro before 2.0.7 allows authentication bypass for static files on case-insensitive filesystems via filename case variations.
Root
Cause The vulnerability lies in Apache Shiro's static file serving when deployed on case-insensitive filesystems (e.g., macOS default). Shiro's filter chain definition typically uses lower-case path patterns to restrict access to static resources. However, on case-insensitive filesystems, the operating system treats 'File.html' and 'file.html' as the same file. This mismatch allows an attacker to bypass URL-based security filters by simply varying the case of the filename in the request [1][2].
Exploitation
An attacker with network access to an application using Apache Shiro (before 2.0.7) can request a protected static file using a different case than the configured filter pattern. For example, if the filter restricts access to '/static/file.html', requesting '/static/File.html' will bypass the filter because Shiro performs case-sensitive path matching, while the filesystem serves the same file regardless of case [1][3]. No authentication is required for this bypass; the attacker directly accesses the resource.
Impact
Successful exploitation allows an unauthenticated attacker to read static files that should be protected by Shiro's authentication or authorization filters. This could include sensitive configuration files, user-uploaded documents, or any static content intended to be restricted [1][3]. The severity is considered low because the attack only applies to static files and requires a case-insensitive filesystem.
Mitigation
The issue is fixed in Apache Shiro version 2.0.7 and later. Users can also enable case-insensitive path matching by setting shiro.caseInsensitive=true in application.properties or filterChainResolver.caseInsensitive=true in shiro.ini (introduced in 2.0.7). Upcoming Shiro 3.0.0 will make case-insensitive matching the default [1][2][3]. Upgrade to 2.0.7 or apply the configuration change to prevent bypass attacks.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.shiro:shiro-springMaven | < 2.1.0 | 2.1.0 |
Affected products
2- Apache Software Foundation/Apache Shirov5Range: 0
Patches
13b9638b95749enh: added case-insensitive path filtering
13 files changed · +180 −24
core/src/main/java/org/apache/shiro/util/AntPathMatcher.java+27 −20 modified@@ -69,6 +69,7 @@ public class AntPathMatcher implements PatternMatcher { public static final String DEFAULT_PATH_SEPARATOR = "/"; private String pathSeparator = DEFAULT_PATH_SEPARATOR; + private boolean caseInsensitive; /** @@ -79,6 +80,16 @@ public void setPathSeparator(String pathSeparator) { this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); } + @Override + public boolean isCaseInsensitive() { + return caseInsensitive; + } + + @Override + public void setCaseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + /** * Checks if {@code path} is a pattern (i.e. contains a '*', or '?'). * For example the {@code /foo/**} would return {@code true}, while {@code /bar/} would return {@code false}. @@ -281,11 +292,9 @@ private boolean matchStrings(String pattern, String str) { } for (int i = 0; i <= patIdxEnd; i++) { ch = patArr[i]; - if (ch != '?') { - if (ch != strArr[i]) { - // Character mismatch - return false; - } + if (ch != '?' && checkCase(ch) != checkCase(strArr[i])) { + // Character mismatch + return false; } } // String matches against pattern @@ -300,12 +309,11 @@ private boolean matchStrings(String pattern, String str) { // Process characters before first star while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { - if (ch != '?') { - if (ch != strArr[strIdxStart]) { - // Character mismatch - return false; - } + if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxStart])) { + // Character mismatch + return false; } + patIdxStart++; strIdxStart++; } @@ -322,12 +330,11 @@ private boolean matchStrings(String pattern, String str) { // Process characters after last star while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { - if (ch != '?') { - if (ch != strArr[strIdxEnd]) { - // Character mismatch - return false; - } + if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxEnd])) { + // Character mismatch + return false; } + patIdxEnd--; strIdxEnd--; } @@ -366,10 +373,8 @@ private boolean matchStrings(String pattern, String str) { for (int i = 0; i <= strLength - patLength; i++) { for (int j = 0; j < patLength; j++) { ch = patArr[patIdxStart + j + 1]; - if (ch != '?') { - if (ch != strArr[strIdxStart + i + j]) { - continue strLoop; - } + if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxStart + i + j])) { + continue strLoop; } } @@ -434,5 +439,7 @@ public String extractPathWithinPattern(String pattern, String path) { return builder.toString(); } - + private char checkCase(char ch) { + return isCaseInsensitive() ? Character.toLowerCase(ch) : ch; + } }
core/src/main/java/org/apache/shiro/util/PatternMatcher.java+15 −0 modified@@ -39,4 +39,19 @@ public interface PatternMatcher { * <code>false</code> otherwise. */ boolean matches(String pattern, String source); + + /** + * Returns {@code true} if pattern matching should be case-insensitive. + */ + default boolean isCaseInsensitive() { + return false; + } + + /** + * Sets whether pattern matching should be case-insensitive. + * + * @param caseInsensitive {@code true} if pattern matching should be case-insensitive. + */ + default void setCaseInsensitive(boolean caseInsensitive) { + } }
core/src/main/java/org/apache/shiro/util/RegExPatternMatcher.java+2 −0 modified@@ -62,6 +62,7 @@ public boolean matches(String pattern, String source) { * * @return true if regex match should be case-insensitive. */ + @Override public boolean isCaseInsensitive() { return caseInsensitive; } @@ -71,6 +72,7 @@ public boolean isCaseInsensitive() { * * @param caseInsensitive true if patterns should match case-insensitive. */ + @Override public void setCaseInsensitive(boolean caseInsensitive) { this.caseInsensitive = caseInsensitive; }
core/src/test/java/org/apache/shiro/util/AntPathMatcherTests.java+8 −0 modified@@ -332,4 +332,12 @@ void matches() { void isPatternWithNullPath() { assertFalse(pathMatcher.isPattern(null)); } + + @Test + void caseInsensitiveMatch() { + pathMatcher.setCaseInsensitive(true); + assertTrue(pathMatcher.match("/Test/Path", "/test/path")); + assertTrue(pathMatcher.match("/TEST/PATH/*", "/test/path/extra")); + assertFalse(pathMatcher.match("/TEST/PATH", "/different/path")); + } }
support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java+4 −0 modified@@ -56,6 +56,9 @@ public class AbstractShiroWebFilterConfiguration { @Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }") protected String unauthorizedUrl; + @Value("#{ @environment['shiro.caseInsensitive'] ?: false }") + protected boolean caseInsensitive; + protected List<String> globalFilters() { return Collections.singletonList(DefaultFilter.invalidRequest.name()); } @@ -72,6 +75,7 @@ protected ShiroFilterFactoryBean shiroFilterFactoryBean() { filterFactoryBean.setLoginUrl(loginUrl); filterFactoryBean.setSuccessUrl(successUrl); filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl); + filterFactoryBean.setCaseInsensitive(caseInsensitive); filterFactoryBean.setSecurityManager(securityManager); filterFactoryBean.setShiroFilterConfiguration(shiroFilterConfiguration());
support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java+18 −1 modified@@ -135,6 +135,7 @@ public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor { private String loginUrl; private String successUrl; private String unauthorizedUrl; + private boolean caseInsensitive; private AbstractShiroFilter instance; @@ -283,6 +284,21 @@ public void setUnauthorizedUrl(String unauthorizedUrl) { this.unauthorizedUrl = unauthorizedUrl; } + /** + * @return true if filter chain matching should be case insensitive. + */ + public boolean isCaseInsensitive() { + return caseInsensitive; + } + + /** + * Sets whether filter chain matching should be case insensitive. + * @param caseInsensitive true if filter chain matching should be case insensitive. + */ + public void setCaseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + /** * Returns the filterName-to-Filter map of filters available for reference when defining filter chain definitions. * All filter chain definitions will reference filters by the names in this map (i.e. the keys). @@ -409,6 +425,7 @@ public boolean isSingleton() { protected FilterChainManager createFilterChainManager() { DefaultFilterChainManager manager = new DefaultFilterChainManager(); + manager.setCaseInsensitive(caseInsensitive); Map<String, Filter> defaultFilters = manager.getFilters(); //apply global settings if necessary: for (Filter filter : defaultFilters.values()) { @@ -489,7 +506,7 @@ protected AbstractShiroFilter createInstance() throws Exception { //Expose the constructed FilterChainManager by first wrapping it in a // FilterChainResolver implementation. The AbstractShiroFilter implementations // do not know about FilterChainManagers - only resolvers: - PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); + PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive); chainResolver.setFilterChainManager(manager); //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy+49 −1 modified@@ -25,7 +25,12 @@ import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition import org.apache.shiro.spring.web.config.ShiroWebFilterConfiguration import org.apache.shiro.web.filter.InvalidRequestFilter +import org.apache.shiro.web.filter.PathConfigProcessor import org.apache.shiro.web.filter.mgt.FilterChainManager +import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver +import org.apache.shiro.web.mgt.DefaultWebSecurityManager +import org.apache.shiro.web.servlet.AbstractShiroFilter +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired @@ -47,6 +52,7 @@ import static org.hamcrest.Matchers.contains import static org.hamcrest.Matchers.instanceOf import static org.hamcrest.Matchers.notNullValue import static org.hamcrest.MatcherAssert.assertThat +import static org.hamcrest.Matchers.is; /** * Test ShiroWebFilterConfiguration creates a ShiroFilterFactoryBean that contains Servlet filters that are available for injection. @@ -62,6 +68,13 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests { @Autowired private ShiroFilterFactoryBean shiroFilterFactoryBean + private static final ThreadLocal<Boolean> caseInsensitiveCalled = ThreadLocal.withInitial { false } + + @AfterEach + void tearDown() { + caseInsensitiveCalled.remove() + } + @Test void testShiroFilterFactoryBeanContainsSpringFilters() { @@ -73,6 +86,31 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests { assertThat filterChainManager.getChain("/test-me"), contains(instanceOf(InvalidRequestFilter), instanceOf(ExpectedTestFilter)) } + @Test + void caseInsensitiveChainManager() { + shiroFilterFactoryBean.setCaseInsensitive true + FilterChainManager filterChainManager = shiroFilterFactoryBean.createFilterChainManager() + assertThat filterChainManager.caseInsensitive, is(true) + } + + @Test + void caseInsensitiveResolverAndPathMatcher() { + shiroFilterFactoryBean.setCaseInsensitive true + shiroFilterFactoryBean.setSecurityManager new DefaultWebSecurityManager() + AbstractShiroFilter filter = shiroFilterFactoryBean.getObject() + PathMatchingFilterChainResolver resolver = filter.filterChainResolver; + assertThat resolver.caseInsensitive, is(true) + assertThat resolver.pathMatcher.caseInsensitive, is(true) + assertThat caseInsensitiveCalled.get(), is(true) + } + + @Test + void caseInsensitivePathConfigProcessor() { + shiroFilterFactoryBean.setCaseInsensitive true + shiroFilterFactoryBean.createFilterChainManager() + assertThat caseInsensitiveCalled.get(), is(true) + } + @Configuration static class FilterConfiguration { @@ -91,7 +129,7 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests { } } - static class ExpectedTestFilter implements Filter { + static class ExpectedTestFilter implements Filter, PathConfigProcessor { @Override void init(FilterConfig filterConfig) throws ServletException {} @@ -100,5 +138,15 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests { @Override void destroy() {} + + @Override + Filter processPathConfig(String path, String config) { + return null + } + + @Override + void setCaseInsensitive(boolean caseInsensitive) { + caseInsensitiveCalled.set caseInsensitive + } } }
web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java+12 −2 modified@@ -61,6 +61,8 @@ public class IniFilterChainResolverFactory extends IniFactorySupport<FilterChain private List<String> globalFilters = Collections.singletonList(DefaultFilter.invalidRequest.name()); + private boolean caseInsensitive; + public IniFilterChainResolverFactory() { super(); } @@ -90,6 +92,14 @@ public void setGlobalFilters(List<String> globalFilters) { this.globalFilters = globalFilters; } + public boolean isCaseInsensitive() { + return caseInsensitive; + } + + public void setCaseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + protected FilterChainResolver createInstance(Ini ini) { FilterChainResolver filterChainResolver = createDefaultInstance(); if (filterChainResolver instanceof PathMatchingFilterChainResolver) { @@ -103,9 +113,9 @@ protected FilterChainResolver createInstance(Ini ini) { protected FilterChainResolver createDefaultInstance() { FilterConfig filterConfig = getFilterConfig(); if (filterConfig != null) { - return new PathMatchingFilterChainResolver(filterConfig); + return new PathMatchingFilterChainResolver(filterConfig).caseInsensitive(caseInsensitive); } else { - return new PathMatchingFilterChainResolver(); + return new PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive); } }
web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java+8 −0 modified@@ -66,6 +66,8 @@ public class DefaultFilterChainManager implements FilterChainManager { */ private Map<String, NamedFilterList> filterChains; + private boolean caseInsensitive; + public DefaultFilterChainManager() { this.filters = new LinkedHashMap<String, Filter>(); this.filterChains = new LinkedHashMap<String, NamedFilterList>(); @@ -121,6 +123,11 @@ public Filter getFilter(String name) { return this.filters.get(name); } + @Override + public void setCaseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + public void addFilter(String name, Filter filter) { addFilter(name, filter, false); } @@ -327,6 +334,7 @@ protected void applyChainConfig(String chainName, Filter filter, String chainSpe } if (filter instanceof PathConfigProcessor) { ((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig); + ((PathConfigProcessor) filter).setCaseInsensitive(caseInsensitive); } else { if (StringUtils.hasText(chainSpecificFilterConfig)) { //they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor
web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java+6 −0 modified@@ -220,4 +220,10 @@ public interface FilterChainManager { * @since 1.6 */ void setGlobalFilters(List<String> globalFilterNames) throws ConfigurationException; + + /** + * Sets whether or not path matching should be case insensitive. + * @param caseInsensitive boolean value indicating whether path matching should be case insensitive. + */ + default void setCaseInsensitive(boolean caseInsensitive) { } }
web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java+18 −0 modified@@ -61,6 +61,11 @@ public PathMatchingFilterChainResolver(FilterConfig filterConfig) { this.filterChainManager = new DefaultFilterChainManager(filterConfig); } + public PathMatchingFilterChainResolver caseInsensitive(boolean caseInsensitive) { + setCaseInsensitive(caseInsensitive); + return this; + } + /** * Returns the {@code PatternMatcher} used when determining if an incoming request's path * matches a configured filter chain. Unless overridden, the @@ -85,6 +90,19 @@ public void setPathMatcher(PatternMatcher pathMatcher) { this.pathMatcher = pathMatcher; } + public boolean isCaseInsensitive() { + return pathMatcher != null && pathMatcher.isCaseInsensitive(); + } + + public void setCaseInsensitive(boolean caseInsensitive) { + if (pathMatcher != null) { + pathMatcher.setCaseInsensitive(caseInsensitive); + } + if (filterChainManager != null) { + filterChainManager.setCaseInsensitive(caseInsensitive); + } + } + public FilterChainManager getFilterChainManager() { return filterChainManager; }
web/src/main/java/org/apache/shiro/web/filter/PathConfigProcessor.java+6 −0 modified@@ -36,4 +36,10 @@ public interface PathConfigProcessor { * @return the {@code Filter} that should execute for the given path/config combination. */ Filter processPathConfig(String path, String config); + + /** + * Sets whether the path matching performed by this processor is case insensitive. + * @param caseInsensitive true if case insensitive, false otherwise + */ + default void setCaseInsensitive(boolean caseInsensitive) { } }
web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java+7 −0 modified@@ -91,6 +91,13 @@ public Filter processPathConfig(String path, String config) { return this; } + @Override + public void setCaseInsensitive(boolean caseInsensitive) { + if (pathMatcher != null) { + pathMatcher.setCaseInsensitive(caseInsensitive); + } + } + /** * Returns the context path within the application based on the specified <code>request</code>. * <p/>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-c244-p6m5-vqj6ghsaADVISORY
- lists.apache.org/thread/5jjf0hnjcol58z2m5y255c7scz1lnp8kghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-23903ghsaADVISORY
- www.openwall.com/lists/oss-security/2026/02/08/1ghsaWEB
- github.com/apache/shiro/commit/3b9638b957495004599aeaf24ba8949e309f26e8ghsaWEB
News mentions
0No linked articles in our index yet.