Apache shiro RegExPatternMatcher permission bypass vulnerability (CVE-2022-32532)

Vulnerability description

On June 29, 2022, Apache officially disclosed the Apache Shiro (CVE-2022-32532) permission bypass vulnerability.
When RegexRequestMatcher is used for permission configuration in Apache Shiro, and the regular expression carries “.”, an unauthorized remote attacker can construct malicious data packets Identity authentication is bypassed, causing the configured permission verification to fail.

Related introduction

Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, encryption and session management and can be used to protect any application – from command line applications to mobile Apps to the largest web and enterprise applications.

Affected versions

Safe version: Apache Shiro = 1.9.1
Affected versions: Apache Shiro < 1.9.1

loopholes

Post a screenshot of the vulnerability I encountered, as shown below:

Follow the prompts to upgrade the version of the relevant package to 1.9.1, package the program and deploy it.
It is found that the vulnerability will still be scanned and upgraded to 1.10.0, 1.11.0, 1.12.0, until version 1.12.0 The vulnerability has not appeared.

I thought it was a simple version upgrade, but I found that methods such as createToken in the login request were executed twice each time;

tracking code

  • shiroFilterFactoryBean method in Shiro configuration class ShiroConfig
 @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
    {<!-- -->
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // Shiro's core security interface, this attribute is required
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // If identity authentication fails, jump to the configuration of the login page.
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // If permission authentication fails, jump to the specified page.
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        //Shiro connection constraint configuration, that is, the definition of filter chain
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //Set anonymous access to static resources
        ...........
        return shiroFilterFactoryBean;
    }
  • Follow up further
private void applyGlobalPropertiesIfNecessary(Filter filter) {<!-- -->
        this.applyLoginUrlIfNecessary(filter);
        this.applySuccessUrlIfNecessary(filter);
        this.applyUnauthorizedUrlIfNecessary(filter);
        if (filter instanceof OncePerRequestFilter) {<!-- -->
            ((OncePerRequestFilter)filter).setFilterOncePerRequest(this.filterConfiguration.isFilterOncePerRequest());
        }

    }

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {<!-- -->
        if (bean instanceof Filter) {<!-- -->
            log.debug("Found filter chain candidate filter '{}'", beanName);
            Filter filter = (Filter)bean;
            this.applyGlobalPropertiesIfNecessary(filter);
            this.getFilters().put(beanName, filter);
        } else {<!-- -->
            log.trace("Ignoring non-Filter bean '{}'", beanName);
        }
        return bean;
    }

As can be seen from the above method, the postProcessBeforeInitialization method is executed when the bean is initialized, and setFilterOncePerRequest in the custom login filter is set to the value given in the ShiroFilterConfiguration instance;
Its value defaults to false, and the once-only execution mechanism of OncePerRequestFilter is not enabled.

The core method of the OncePerRequestFilter class (difference between versions 1.9.0 and 1.12.0)

  • When Apache Shiro = 1.9.0, OncePerRequestFilter class source code
package org.apache.shiro.web.servlet;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class OncePerRequestFilter extends NameableFilter {<!-- -->
    private static final Logger log = LoggerFactory.getLogger(OncePerRequestFilter.class);
    public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
    private boolean enabled = true;

    public OncePerRequestFilter() {<!-- -->}
    public boolean isEnabled() {<!-- --> return this.enabled; }
    public void setEnabled(boolean enabled) {<!-- --> this.enabled = enabled; }

    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {<!-- -->
        String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
        if (request.getAttribute(alreadyFilteredAttributeName) != null) {<!-- -->
            log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", this.getName());
            filterChain.doFilter(request, response);
        } else if (this.isEnabled(request, response) & amp; & amp; !this.shouldNotFilter(request)) {<!-- -->
            log.trace("Filter '{}' not yet executed. Executing now.", this.getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
            try {<!-- -->
                this.doFilterInternal(request, response, filterChain);
            } finally {<!-- -->
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        } else {<!-- -->
            log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", this.getName());
            filterChain.doFilter(request, response);
        }
    }

    protected boolean isEnabled(ServletRequest request, ServletResponse response) throws ServletException, IOException {<!-- -->
        return this.isEnabled();
    }

    protected String getAlreadyFilteredAttributeName() {<!-- -->
        String name = this.getName();
        if (name == null) {<!-- -->
            name = this.getClass().getName();
        }
        return name + ".FILTERED";
    }

    /** @deprecated */
    @Deprecated
    protected boolean shouldNotFilter(ServletRequest request) throws ServletException {<!-- -->
        return false;
    }

    protected abstract void doFilterInternal(ServletRequest var1, ServletResponse var2, FilterChain var3) throws ServletException, IOException;
}
  • When Apache Shiro = 1.12.0, OncePerRequestFilter class source code
package org.apache.shiro.web.servlet;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class OncePerRequestFilter extends NameableFilter {<!-- -->
    private static final Logger log = LoggerFactory.getLogger(OncePerRequestFilter.class);
    public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
    private boolean enabled = true;
    private boolean filterOncePerRequest = false;
    
    public OncePerRequestFilter() {<!-- -->}
    public boolean isEnabled() {<!-- --> return this.enabled; }
    public void setEnabled(boolean enabled) {<!-- --> this.enabled = enabled; }
    public boolean isFilterOncePerRequest() {<!-- --> return this.filterOncePerRequest; }
    public void setFilterOncePerRequest(boolean filterOncePerRequest) {<!-- -->
        this.filterOncePerRequest = filterOncePerRequest;
    }

    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {<!-- -->
        String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
        if (request.getAttribute(alreadyFilteredAttributeName) != null & amp; & amp; this.filterOncePerRequest) {<!-- -->
            log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", this.getName());
            filterChain.doFilter(request, response);
        } else if (this.isEnabled(request, response) & amp; & amp; !this.shouldNotFilter(request)) {<!-- -->
            log.trace("Filter '{}' not yet executed. Executing now.", this.getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
            try {<!-- -->
                this.doFilterInternal(request, response, filterChain);
            } finally {<!-- -->
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        } else {<!-- -->
            log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", this.getName());
            filterChain.doFilter(request, response);
        }
    }

    protected boolean isEnabled(ServletRequest request, ServletResponse response) throws ServletException, IOException {<!-- -->
        return this.isEnabled();
    }

    protected String getAlreadyFilteredAttributeName() {<!-- -->
        String name = this.getName();
        if (name == null) {<!-- -->
            name = this.getClass().getName();
        }
        return name + ".FILTERED";
    }

    /** @deprecated */
    @Deprecated
    protected boolean shouldNotFilter(ServletRequest request) throws ServletException {<!-- -->
        return false;
    }
    
    protected abstract void doFilterInternal(ServletRequest var1, ServletResponse var2, FilterChain var3) throws ServletException, IOException;
}

Comparing the two versions of the code, we found that when Apache Shiro = version 1.12.0, the third line of code in the doFilter method added & amp; & amp; filterOncePerRequest to judge. This value is determined by ShiroFilterConfiguration > ShiroFilterFactoryBean is passed in all the way, and it is executed after constructing ShiroFilterFactoryBean, which is later than the construction time of the custom Filter, so try to customize the filter It is useless to set it to true in the constructor's constructor or methods such as postxxx, afterxxx.

The problem can only be solved by setting its configuration properties when constructing the ShiroFilterFactoryBean object.

Solution

  • When creating ShiroFilterFactoryBean, give him a ShiroFilterConfiguration instance object and set the setFilterOncePerRequest(true) of this instance
 @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
    {<!-- -->
        ShiroFilterConfiguration config = new ShiroFilterConfiguration();
        //Global configuration whether to enable the once-only execution mechanism of OncePerRequestFilter
        config.setFilterOncePerRequest(Boolean.TRUE);
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setShiroFilterConfiguration(config);
        // Shiro's core security interface, this attribute is required
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // If identity authentication fails, jump to the configuration of the login page.
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // If permission authentication fails, jump to the specified page.
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        //Shiro connection constraint configuration, that is, the definition of filter chain
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //Set anonymous access to static resources
        ...........
        return shiroFilterFactoryBean;
    }

Summarize

Apache Shiro = versions before 1.9.0, the OncePerRequestFilter filter subtype is only executed once by default.
You can now choose whether to enable the once-only execution mechanism of OncePerRequestFilter through global configuration.