VYPR
Moderate severityNVD Advisory· Published Feb 9, 2026· Updated Feb 9, 2026

Apache Shiro: Auth bypass when accessing static files only on case-insensitive filesystems

CVE-2026-23903

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.

PackageAffected versionsPatched versions
org.apache.shiro:shiro-springMaven
< 2.1.02.1.0

Affected products

2
  • Apache/Shirollm-fuzzy
    Range: <2.0.7
  • Apache Software Foundation/Apache Shirov5
    Range: 0

Patches

1
3b9638b95749

enh: added case-insensitive path filtering

https://github.com/apache/shirolprimakJan 31, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.