With attachments.

On Tue, May 04, 2021 at 09:15:50PM -0400, Roberto C. Sánchez wrote:
> I never received a reply to the below request.  Is it possible for
> someone to review the CVE-2020-13933 patch?
> 
> Additionally, I have prepared two patches for CVE-2020-17510, which I
> have attached to this email.  The adaptations were fairly minor, but a
> review would still be appreciated.
> 
> As far as CVE-2020-17523, it looks the affected file
> (support/spring-boot/spring-boot-web-starter/src/main/resources/META-INF/spring.factories)
> was introduced in 1.4.x.  If that is the case, then CVE-2020-17523 would
> not affect 1.3.x versions of shiro.  Do I have that right?
> 
> I'd like to get these changes wrapped up and a review would increase my
> confidence in the adaptations I have made.
> 
> Once these are nailed down (fixes for CVE-2020-13933 and CVE-2020-17510,
> and confirmation that CVE-2020-17523 is not applicable in 1.3.x), then I
> will be in a position to address getting a more recent upstream release
> into Debian.
> 
> Regards,
> 
> -Roberto
> 
> On Wed, Apr 07, 2021 at 03:41:25PM -0400, Roberto C. Sánchez wrote:
> > Hi Brian, Ben, et al.,
> > 
> > I have what I believe to be a correct backport of commit dc194fc977ab to
> > address CVE-2020-13933.  The adaptations/changes include:
> > 
> > - replacing the diamond operator <> with type specifications (the 1.3.x
> >   build uses source compatibility level 1.6)
> > - replacing stream constructs with loops
> > - omitting changes to non-existent tests
> > - adjusting for the absence of the FilterConfig type (which seems to
> >   have been introduced in 1.4)
> > 
> > If someone familiar with the code, the vulnerability, and the fix could
> > review the attached patch that would be very much appreciated.  Also, it
> > would be quite helpful if there was a reproducer or a description of how
> > to exploit the vulnerability to help with validating the fix.  If that
> > cannot be disclosed, then perhaps someone could confirm that this patch
> > does in fact address the vulnerability; that would be helpful.
> > 
> > Next I will start working on CVE-2020-17510 and CVE-2020-17523.  Once I
> > have those sorted out for the existing 1.3.x packages I will turn my
> > attention to seeing about updating to a newer upstream release (which
> > could be a lengthy process).
> > 
> > Regards,
> > 
> > -Roberto
> > 
> > On Wed, Mar 31, 2021 at 03:21:55PM -0400, Roberto C. Sánchez wrote:
> > > Hi Brian,
> > > 
> > > Thanks for your help.  I am working to backport dc194fc977ab to address
> > > CVE-2020-13933 and after that I will move on to the fixes for
> > > CVE-2020-17510 and CVE-2020-17523.
> > > 
> > > As far as the maintainability of a 1.3.x package, upgrading to a newer
> > > version is not an option for two reasons.  First, I am working on
> > > addressing these vulnerabilities in Debian Stretch, which is in the LTS
> > > stage of its lifecycle, making an update to a new upstream release very
> > > unlikely.  Second, the shiro package in Debian was last updated about 2
> > > years ago and even in Debian unstable 1.3.x is the highest available
> > > version.  Any update to a new upstream version would need to start in
> > > the unstable distribution.
> > > 
> > > As an additional complication, the activemq package in Debian depends on
> > > the shiro package, so a shiro update would need to be coordinated in a
> > > that ensures it doesn't break activemq.
> > > 
> > > That said, I will bring the issue up to see if I can get an update
> > > started so that at the least the next Debian stable release has
> > > something more recent.
> > > 
> > > Regards,
> > > 
> > > -Roberto
> > > 
> > > On Tue, Mar 16, 2021 at 05:50:50PM -0400, Brian Demers wrote:
> > > > Hey Roberto,
> > > > 
> > > > Sorry about the delay on this one, I originally thought we had answered
> > > > your question.
> > > > 
> > > > The commit you are looking for is
> > > > https://github.com/apache/shiro/commit/dc194fc977ab6cfbf3c1ecb085e2bac5db14af6d
> > > > 
> > > > If you are maintaining a 1.3.x package this is going to become more
> > > > difficult, is it possible to deprecate it and move to a recent version?
> > > > 
> > > > 
> > > > 
> > > > On Sat, Jan 30, 2021 at 4:45 PM Roberto C. Sánchez <[email protected]>
> > > > wrote:
> > > > 
> > > > > Bump.
> > > > >
> > > > > On Tue, Dec 22, 2020 at 09:30:47AM -0500, Roberto C. Sánchez wrote:
> > > > > > On Mon, Dec 21, 2020 at 09:33:44PM +0100, Benjamin Marwell wrote:
> > > > > > > Hi Roberto,
> > > > > > >
> > > > > > > after talking to the PMC chair, I can give you three commit links.
> > > > > > >
> > > > > > >
> > > > > https://github.com/apache/shiro/commit/042c59356cc6442345a9f935aed3e7603cb4dfad
> > > > > > >
> > > > > https://github.com/apache/shiro/commit/5b1add9a4c4ed046b52cf2132ed0f264a22caf1d
> > > > > > >
> > > > > https://github.com/apache/shiro/commit/1b9d8d99cd6d50d7114916508a13677a0fe6f345
> > > > > > >
> > > > > > > I guess it is quite obvious what is inside these commits.
> > > > > > >
> > > > > > Hi Ben,
> > > > > >
> > > > > > This commits seem to have been made after the 1.6.0 release and 
> > > > > > before
> > > > > > the 1.7.0 release.  My belief is that they address CVE-2020-17510.  
> > > > > > Can
> > > > > > you tell me if dc194fc977ab6cfbf3c1ecb085e2bac5db14af6d is the 
> > > > > > commit
> > > > > > that addresses CVE-2020-13933?  Are there other commits that go 
> > > > > > along
> > > > > > with it to completely remedy CVE-2020-13933?
> > > > > >
> > > > > > Regards,
> > > > > >
> > > > > > -Roberto
> > > > > >
> > > > > > --
> > > > > > Roberto C. Sánchez
> > > > >
> > > > > --
> > > > > Roberto C. Sánchez
> > > > >
> > > 
> > > -- 
> > > Roberto C. Sánchez
> > > http://people.connexer.com/~roberto
> > > http://www.connexer.com
> > 
> > -- 
> > Roberto C. Sánchez
> > http://people.connexer.com/~roberto
> > http://www.connexer.com
> 
> > From dc194fc977ab6cfbf3c1ecb085e2bac5db14af6d Mon Sep 17 00:00:00 2001
> > From: Brian Demers <[email protected]>
> > Date: Tue, 7 Jul 2020 21:06:35 -0400
> > Subject: [PATCH] Add a feature to allow for global filters
> > 
> > Adds new filter to block invalid requests
> > ---
> >  .../shiro/guice/web/ShiroWebModule.java       |  25 ++-
> >  .../shiro/guice/web/ShiroWebModuleTest.java   | 153 ++++++++++++++++++
> >  .../ShiroWebFilterConfiguration.java          |   8 +
> >  .../web/ConfiguredGlobalFiltersTest.groovy    | 104 ++++++++++++
> >  .../web/DisabledGlobalFiltersTest.groovy      |  64 ++++++++
> >  ...ShiroWebSpringAutoConfigurationTest.groovy |  30 +++-
> >  ...roWebAutoConfigurationTestApplication.java |   4 +-
> >  .../spring/web/ShiroFilterFactoryBean.java    |  23 +++
> >  .../config/AbstractShiroWebConfiguration.java |   3 -
> >  .../AbstractShiroWebFilterConfiguration.java  |   9 +-
> >  .../config/ShiroWebFilterConfiguration.java   |   6 +
> >  .../ShiroWebFilterConfigurationTest.groovy    |   3 +-
> >  .../web/ShiroFilterFactoryBeanTest.java       |   8 +-
> >  .../config/IniFilterChainResolverFactory.java |  18 +++
> >  .../web/filter/InvalidRequestFilter.java      | 124 ++++++++++++++
> >  .../shiro/web/filter/mgt/DefaultFilter.java   |   4 +-
> >  .../filter/mgt/DefaultFilterChainManager.java |  37 ++++-
> >  .../web/filter/mgt/FilterChainManager.java    |  22 +++
> >  .../web/servlet/AbstractShiroFilter.java      |   1 +
> >  .../IniFilterChainResolverFactoryTest.groovy  |  26 +++
> >  .../web/env/IniWebEnvironmentTest.groovy      |  69 ++++++++
> >  .../filter/InvalidRequestFilterTest.groovy    | 106 ++++++++++++
> >  .../mgt/DefaultFilterChainManagerTest.groovy  |  52 ++++++
> >  .../org/apache/shiro/web/env/FilterStub.java  |  45 ++++++
> >  24 files changed, 925 insertions(+), 19 deletions(-)
> >  create mode 100644 
> > support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy
> >  create mode 100644 
> > support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy
> >  create mode 100644 
> > web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
> >  create mode 100644 
> > web/src/test/groovy/org/apache/shiro/web/filter/InvalidRequestFilterTest.groovy
> >  create mode 100644 
> > web/src/test/java/org/apache/shiro/web/env/FilterStub.java
> > 
> > --- 
> > a/support/guice/src/main/java/org/apache/shiro/guice/web/ShiroWebModule.java
> > +++ 
> > b/support/guice/src/main/java/org/apache/shiro/guice/web/ShiroWebModule.java
> > @@ -18,9 +18,13 @@
> >   */
> >  package org.apache.shiro.guice.web;
> >  
> > +import java.util.ArrayList;
> > +import java.util.Arrays;
> >  import java.util.Collection;
> > +import java.util.Collections;
> >  import java.util.HashMap;
> >  import java.util.LinkedHashMap;
> > +import java.util.List;
> >  import java.util.Map;
> >  
> >  import javax.servlet.Filter;
> > @@ -32,6 +36,7 @@
> >  import org.apache.shiro.mgt.SecurityManager;
> >  import org.apache.shiro.session.mgt.SessionManager;
> >  import org.apache.shiro.web.env.WebEnvironment;
> > +import org.apache.shiro.web.filter.InvalidRequestFilter;;
> >  import org.apache.shiro.web.filter.PathMatchingFilter;
> >  import org.apache.shiro.web.filter.authc.AnonymousFilter;
> >  import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
> > @@ -86,7 +91,8 @@
> >      public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
> >      @SuppressWarnings({"UnusedDeclaration"})
> >      public static final Key<UserFilter> USER = Key.get(UserFilter.class);
> > -
> > +    @SuppressWarnings({"UnusedDeclaration"})
> > +    public static final Key<InvalidRequestFilter> INVALID_REQUEST = 
> > Key.get(InvalidRequestFilter.class);
> >  
> >      static final String NAME = "SHIRO";
> >  
> > @@ -123,6 +129,12 @@
> >          };
> >      }
> >  
> > +    public List<Key<? extends Filter>> globalFilters() {
> > +        ArrayList<Key<? extends Filter>> filters = new ArrayList<Key<? 
> > extends Filter>>();
> > +        filters.add(INVALID_REQUEST);
> > +        return Collections.unmodifiableList(filters);
> > +    }
> > +
> >      @Override
> >      protected final void configureShiro() {
> >          bindBeanType(TypeLiteral.get(ServletContext.class), 
> > Key.get(ServletContext.class, Names.named(NAME)));
> > @@ -134,6 +146,12 @@
> >  
> >          this.configureShiroWeb();
> >  
> > +        // add default matching route if not already set
> > +        if (!filterChains.containsKey("/**")) {
> > +            // no config, this will add only the global filters
> > +            this.addFilterChain("/**", new Key[0]);
> > +        }
> > +
> >          setupFilterChainConfigs();
> >  
> >          bind(FilterChainResolver.class).toProvider(new 
> > FilterChainResolverProvider(filterChains));
> > @@ -143,8 +161,15 @@
> >          Map<Key<? extends PathMatchingFilter>, Map<String, String>> 
> > configs = new HashMap<Key<? extends PathMatchingFilter>, Map<String, 
> > String>>();
> >  
> >          for (Map.Entry<String, Key<? extends Filter>[]> filterChain : 
> > filterChains.entrySet()) {
> > -            for (int i = 0; i < filterChain.getValue().length; i++) {
> > -                Key<? extends Filter> key = filterChain.getValue()[i];
> > +            List<Key<? extends Filter>> globalFilters = 
> > this.globalFilters();
> > +            Key<? extends Filter>[] pathFilters = filterChain.getValue();
> > +
> > +            // merge the global filters and the path specific filters
> > +            List<Key<? extends Filter>> filterConfigs = new 
> > ArrayList<Key<? extends Filter>>(globalFilters.size() + pathFilters.length);
> > +            filterConfigs.addAll(globalFilters);
> > +            filterConfigs.addAll(Arrays.asList(pathFilters));
> > +            for (int i = 0; i < filterConfigs.size(); i++) {
> > +                Key<? extends Filter> key = filterConfigs.get(i);
> >                  if (key instanceof FilterConfigKey) {
> >                      FilterConfigKey<? extends PathMatchingFilter> 
> > configKey = (FilterConfigKey<? extends PathMatchingFilter>) key;
> >                      key = configKey.getKey();
> > --- 
> > a/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java
> > +++ 
> > b/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java
> > @@ -25,8 +25,10 @@
> >  import org.apache.shiro.util.StringUtils;
> >  import org.apache.shiro.web.config.IniFilterChainResolverFactory;
> >  import org.apache.shiro.web.filter.AccessControlFilter;
> > +import org.apache.shiro.web.filter.InvalidRequestFilter;
> >  import org.apache.shiro.web.filter.authc.AuthenticationFilter;
> >  import org.apache.shiro.web.filter.authz.AuthorizationFilter;
> > +import org.apache.shiro.web.filter.mgt.DefaultFilter;
> >  import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
> >  import org.apache.shiro.web.filter.mgt.FilterChainManager;
> >  import org.apache.shiro.web.filter.mgt.FilterChainResolver;
> > @@ -41,7 +43,9 @@
> >  import org.springframework.beans.factory.config.BeanPostProcessor;
> >  
> >  import javax.servlet.Filter;
> > +import java.util.ArrayList;
> >  import java.util.LinkedHashMap;
> > +import java.util.List;
> >  import java.util.Map;
> >  
> >  /**
> > @@ -121,6 +125,8 @@
> >  
> >      private Map<String, Filter> filters;
> >  
> > +    private List<String> globalFilters;
> > +
> >      private Map<String, String> filterChainDefinitionMap; 
> > //urlPathExpression_to_comma-delimited-filter-chain-definition
> >  
> >      private String loginUrl;
> > @@ -131,6 +137,8 @@
> >  
> >      public ShiroFilterFactoryBean() {
> >          this.filters = new LinkedHashMap<String, Filter>();
> > +        this.globalFilters = new ArrayList<String>();
> > +        this.globalFilters.add(DefaultFilter.invalidRequest.name());
> >          this.filterChainDefinitionMap = new LinkedHashMap<String, 
> > String>(); //order matters!
> >      }
> >  
> > @@ -332,6 +340,14 @@
> >      }
> >  
> >      /**
> > +     * Sets the list of filters that will be executed against every 
> > request.  Defaults to the {@link InvalidRequestFilter} which will block 
> > known invalid request attacks.
> > +     * @param globalFilters the list of filters to execute before specific 
> > path filters.
> > +     */
> > +    public void setGlobalFilters(List<String> globalFilters) {
> > +        this.globalFilters = globalFilters;
> > +    }
> > +
> > +    /**
> >       * Lazily creates and returns a {@link AbstractShiroFilter} concrete 
> > instance via the
> >       * {@link #createInstance} method.
> >       *
> > @@ -388,6 +404,9 @@
> >              }
> >          }
> >  
> > +        // set the global filters
> > +        manager.setGlobalFilters(this.globalFilters);
> > +
> >          //build up the chains:
> >          Map<String, String> chains = getFilterChainDefinitionMap();
> >          if (!CollectionUtils.isEmpty(chains)) {
> > @@ -398,6 +417,9 @@
> >              }
> >          }
> >  
> > +        // create the default chain, to match anything the path matching 
> > would have missed
> > +        manager.createDefaultChain("/**"); // TODO this assumes ANT path 
> > matching, which might be OK here
> > +
> >          return manager;
> >      }
> >  
> > --- 
> > a/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
> > +++ 
> > b/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
> > @@ -24,6 +24,7 @@
> >  import org.apache.shiro.config.ReflectionBuilder;
> >  import org.apache.shiro.util.CollectionUtils;
> >  import org.apache.shiro.util.Factory;
> > +import org.apache.shiro.web.filter.mgt.DefaultFilter;
> >  import org.apache.shiro.web.filter.mgt.FilterChainManager;
> >  import org.apache.shiro.web.filter.mgt.FilterChainResolver;
> >  import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
> > @@ -32,7 +33,9 @@
> >  
> >  import javax.servlet.Filter;
> >  import javax.servlet.FilterConfig;
> > +import java.util.Collections;
> >  import java.util.LinkedHashMap;
> > +import java.util.List;
> >  import java.util.Map;
> >  
> >  /**
> > @@ -49,6 +52,8 @@
> >  
> >      private FilterConfig filterConfig;
> >  
> > +    private List<String> globalFilters = 
> > Collections.singletonList(DefaultFilter.invalidRequest.name());
> > +
> >      private Map<String, ?> defaultBeans;
> >  
> >      public IniFilterChainResolverFactory() {
> > @@ -72,6 +77,14 @@
> >          this.filterConfig = filterConfig;
> >      }
> >  
> > +    public List<String> getGlobalFilters() {
> > +        return globalFilters;
> > +    }
> > +
> > +    public void setGlobalFilters(List<String> globalFilters) {
> > +        this.globalFilters = globalFilters;
> > +    }
> > +
> >      protected FilterChainResolver createInstance(Ini ini) {
> >          FilterChainResolver filterChainResolver = createDefaultInstance();
> >          if (filterChainResolver instanceof 
> > PathMatchingFilterChainResolver) {
> > @@ -122,9 +135,14 @@
> >          //add the filters to the manager:
> >          registerFilters(filters, manager);
> >  
> > +        manager.setGlobalFilters(getGlobalFilters());
> > +
> >          //urls section:
> >          section = ini.getSection(URLS);
> >          createChains(section, manager);
> > +
> > +        // create the default chain, to match anything the path matching 
> > would have missed
> > +        manager.createDefaultChain("/**"); // TODO this assumes ANT path 
> > matching
> >      }
> >  
> >      protected void registerFilters(Map<String, Filter> filters, 
> > FilterChainManager manager) {
> > --- /dev/null
> > +++ 
> > b/web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
> > @@ -0,0 +1,136 @@
> > +/*
> > + * 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.web.filter;
> > +
> > +import org.apache.shiro.web.util.WebUtils;
> > +
> > +import javax.servlet.ServletRequest;
> > +import javax.servlet.ServletResponse;
> > +import java.util.Arrays;
> > +import java.util.Collections;
> > +import java.util.List;
> > +
> > +/**
> > + * A request filter that blocks malicious requests. Invalid request will 
> > respond with a 400 response code.
> > + * <p>
> > + * This filter checks and blocks the request if the following characters 
> > are found in the request URI:
> > + * <ul>
> > + *     <li>Semicolon - can be disabled by setting {@code blockSemicolon = 
> > false}</li>
> > + *     <li>Backslash - can be disabled by setting {@code blockBackslash = 
> > false}</li>
> > + *     <li>Non-ASCII characters - can be disabled by setting {@code 
> > blockNonAscii = false}, the ability to disable this check will be removed 
> > in future version.</li>
> > + * </ul>
> > + *
> > + * @see <a 
> > href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html";>This
> >  class was inspired by Spring Security StrictHttpFirewall</a>
> > + * @since 1.6
> > + */
> > +public class InvalidRequestFilter extends AccessControlFilter {
> > +
> > +    private static final List<String> SEMICOLON = 
> > Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
> > +
> > +    private static final List<String> BACKSLASH = 
> > Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
> > +
> > +    private boolean blockSemicolon = true;
> > +
> > +    private boolean blockBackslash = true;
> > +
> > +    private boolean blockNonAscii = true;
> > +
> > +    @Override
> > +    protected boolean isAccessAllowed(ServletRequest request, 
> > ServletResponse response, Object mappedValue) throws Exception {
> > +        String uri = WebUtils.toHttp(request).getRequestURI();
> > +        return !containsSemicolon(uri)
> > +            && !containsBackslash(uri)
> > +            && !containsNonAsciiCharacters(uri);
> > +    }
> > +
> > +    @Override
> > +    protected boolean onAccessDenied(ServletRequest request, 
> > ServletResponse response) throws Exception {
> > +        WebUtils.toHttp(response).sendError(400, "Invalid request");
> > +        return false;
> > +    }
> > +
> > +    private boolean containsSemicolon(String uri) {
> > +        if (isBlockSemicolon()) {
> > +            int length = uri.length();
> > +            for (int i = 0; i < length; i++) {
> > +                char c = uri.charAt(i);
> > +                if (c == ';') {
> > +                    return true;
> > +                }
> > +            }
> > +        }
> > +        return false;
> > +    }
> > +
> > +    private boolean containsBackslash(String uri) {
> > +        if (isBlockBackslash()) {
> > +            int length = uri.length();
> > +            for (int i = 0; i < length; i++) {
> > +                char c = uri.charAt(i);
> > +                if (c == '\\') {
> > +                    return true;
> > +                }
> > +            }
> > +        }
> > +        return false;
> > +    }
> > +
> > +    private boolean containsNonAsciiCharacters(String uri) {
> > +        if (isBlockNonAscii()) {
> > +            return !containsOnlyPrintableAsciiCharacters(uri);
> > +        }
> > +        return false;
> > +    }
> > +
> > +    private static boolean containsOnlyPrintableAsciiCharacters(String 
> > uri) {
> > +        int length = uri.length();
> > +        for (int i = 0; i < length; i++) {
> > +            char c = uri.charAt(i);
> > +            if (c < '\u0020' || c > '\u007e') {
> > +                return false;
> > +            }
> > +        }
> > +        return true;
> > +    }
> > +
> > +    public boolean isBlockSemicolon() {
> > +        return blockSemicolon;
> > +    }
> > +
> > +    public void setBlockSemicolon(boolean blockSemicolon) {
> > +        this.blockSemicolon = blockSemicolon;
> > +    }
> > +
> > +    public boolean isBlockBackslash() {
> > +        return blockBackslash;
> > +    }
> > +
> > +    public void setBlockBackslash(boolean blockBackslash) {
> > +        this.blockBackslash = blockBackslash;
> > +    }
> > +
> > +    public boolean isBlockNonAscii() {
> > +        return blockNonAscii;
> > +    }
> > +
> > +    public void setBlockNonAscii(boolean blockNonAscii) {
> > +        this.blockNonAscii = blockNonAscii;
> > +    }
> > +}
> > --- a/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilter.java
> > +++ b/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilter.java
> > @@ -19,6 +19,7 @@
> >  package org.apache.shiro.web.filter.mgt;
> >  
> >  import org.apache.shiro.util.ClassUtils;
> > +import org.apache.shiro.web.filter.InvalidRequestFilter;
> >  import org.apache.shiro.web.filter.authc.*;
> >  import org.apache.shiro.web.filter.authz.*;
> >  import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
> > @@ -47,7 +48,8 @@
> >      rest(HttpMethodPermissionFilter.class),
> >      roles(RolesAuthorizationFilter.class),
> >      ssl(SslFilter.class),
> > -    user(UserFilter.class);
> > +    user(UserFilter.class),
> > +    invalidRequest(InvalidRequestFilter.class);
> >  
> >      private final Class<? extends Filter> filterClass;
> >  
> > --- 
> > a/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java
> > +++ 
> > b/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java
> > @@ -30,8 +30,10 @@
> >  import javax.servlet.FilterChain;
> >  import javax.servlet.FilterConfig;
> >  import javax.servlet.ServletException;
> > +import java.util.ArrayList;
> >  import java.util.Collections;
> >  import java.util.LinkedHashMap;
> > +import java.util.List;
> >  import java.util.Map;
> >  import java.util.Set;
> >  
> > @@ -52,17 +54,21 @@
> >  
> >      private Map<String, Filter> filters; //pool of filters available for 
> > creating chains
> >  
> > +    private List<String> globalFilterNames; // list of filters to prepend 
> > to every chain
> > +
> >      private Map<String, NamedFilterList> filterChains; //key: chain name, 
> > value: chain
> >  
> >      public DefaultFilterChainManager() {
> >          this.filters = new LinkedHashMap<String, Filter>();
> >          this.filterChains = new LinkedHashMap<String, NamedFilterList>();
> > +        this.globalFilterNames = new ArrayList<String>();
> >          addDefaultFilters(false);
> >      }
> >  
> >      public DefaultFilterChainManager(FilterConfig filterConfig) {
> >          this.filters = new LinkedHashMap<String, Filter>();
> >          this.filterChains = new LinkedHashMap<String, NamedFilterList>();
> > +        this.globalFilterNames = new ArrayList<String>();
> >          setFilterConfig(filterConfig);
> >          addDefaultFilters(true);
> >      }
> > @@ -115,6 +121,17 @@
> >          addFilter(name, filter, init, true);
> >      }
> >  
> > +    public void createDefaultChain(String chainName) {
> > +        // only create the defaultChain if we don't have a chain with this 
> > name already
> > +        // (the global filters will already be in that chain)
> > +        if (!getChainNames().contains(chainName) && 
> > !CollectionUtils.isEmpty(globalFilterNames)) {
> > +            // add each of global filters
> > +            for (String filterName : globalFilterNames) {
> > +                addToChain(chainName, filterName);
> > +            }
> > +        }
> > +    }
> > +
> >      public void createChain(String chainName, String chainDefinition) {
> >          if (!StringUtils.hasText(chainName)) {
> >              throw new NullPointerException("chainName cannot be null or 
> > empty.");
> > @@ -124,7 +141,14 @@
> >          }
> >  
> >          if (log.isDebugEnabled()) {
> > -            log.debug("Creating chain [" + chainName + "] from String 
> > definition [" + chainDefinition + "]");
> > +            log.debug("Creating chain [" + chainName + "] with global 
> > filters " + globalFilterNames + " and from String definition [" + 
> > chainDefinition + "]");
> > +        }
> > +
> > +        // first add each of global filters
> > +        if (!CollectionUtils.isEmpty(globalFilterNames)) {
> > +            for (String filterName : globalFilterNames) {
> > +                addToChain(chainName, filterName);
> > +            }
> >          }
> >  
> >          //parse the value by tokenizing it to get the resulting 
> > filter-specific config entries
> > @@ -273,6 +297,21 @@
> >          chain.add(filter);
> >      }
> >  
> > +    public void setGlobalFilters(List<String> globalFilterNames) throws 
> > ConfigurationException {
> > +        // validate each filter name
> > +        if (!CollectionUtils.isEmpty(globalFilterNames)) {
> > +            for (String filterName : globalFilterNames) {
> > +                Filter filter = filters.get(filterName);
> > +                if (filter == null) {
> > +                    throw new ConfigurationException("There is no filter 
> > with name '" + filterName +
> > +                                                     "' to apply to the 
> > global filters in the pool of available Filters.  Ensure a " +
> > +                                                     "filter with that 
> > name/path has first been registered with the addFilter method(s).");
> > +                }
> > +                this.globalFilterNames.add(filterName);
> > +            }
> > +        }
> > +    }
> > +
> >      protected void applyChainConfig(String chainName, Filter filter, 
> > String chainSpecificFilterConfig) {
> >          if (log.isDebugEnabled()) {
> >              log.debug("Attempting to apply path [" + chainName + "] to 
> > filter [" + filter + "] " +
> > --- 
> > a/web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java
> > +++ 
> > b/web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java
> > @@ -22,6 +22,7 @@
> >  
> >  import javax.servlet.Filter;
> >  import javax.servlet.FilterChain;
> > +import java.util.List;
> >  import java.util.Map;
> >  import java.util.Set;
> >  
> > @@ -165,6 +166,14 @@
> >      void createChain(String chainName, String chainDefinition);
> >  
> >      /**
> > +     * Creates a chain that should match any non-matched request paths, 
> > typically {@code /**} assuming an {@link AntPathMatcher} I used.
> > +     * @param chainName The name of the chain to create, likely {@code 
> > /**}.
> > +     * @since 1.6
> > +     * @see org.apache.shiro.lang.util.AntPathMatcher AntPathMatcher
> > +     */
> > +    void createDefaultChain(String chainName);
> > +
> > +    /**
> >       * Adds (appends) a filter to the filter chain identified by the given 
> > {@code chainName}.  If there is no chain
> >       * with the given name, a new one is created and the filter will be 
> > the first in the chain.
> >       *
> > @@ -195,4 +204,17 @@
> >       *                                  interface).
> >       */
> >      void addToChain(String chainName, String filterName, String 
> > chainSpecificFilterConfig) throws ConfigurationException;
> > +
> > +    /**
> > +     * Configures the set of named filters that will match all paths.  
> > These filters will match BEFORE explicitly
> > +     * configured filter chains i.e. by calling {@link 
> > #createChain(String, String)}, {@link #addToChain(String, String)}, etc.
> > +     * <br>
> > +     * <strong>Filters configured in this list wll apply to ALL 
> > requests.</strong>
> > +     *
> > +     * @param globalFilterNames         the list of filter names to match 
> > ALL paths.
> > +     * @throws ConfigurationException   if one of the filter names is 
> > invalid and cannot be loaded from the set of
> > +     *                                  configured filters {@link 
> > #getFilters()}}.
> > +     * @since 1.6
> > +     */
> > +    void setGlobalFilters(List<String> globalFilterNames) throws 
> > ConfigurationException;
> >  }
> 
> 
> -- 
> Roberto C. Sánchez

-- 
Roberto C. Sánchez
>From a28300448ae6c4bb78a8ba626b0cacb00f82d5f8 Mon Sep 17 00:00:00 2001
From: Brian Demers <[email protected]>
Date: Thu, 3 Sep 2020 14:58:45 -0400
Subject: [PATCH] Adds configuration to toggle the normalization of backslashes

This is normally handled by the container
Update the InvalidRequestFilter to use WebUtils.ALLOW_BACKSLASH
(new system property: org.apache.shiro.web.ALLOW_BACKSLASH)

Fixes: SHIRO-794
---
 .../web/filter/InvalidRequestFilter.java      | 22 ++++--
 .../org/apache/shiro/web/util/WebUtils.java   |  4 +-
 .../filter/InvalidRequestFilterTest.groovy    | 48 +++++++++++--
 .../apache/shiro/web/util/WebUtilsTest.groovy | 52 ++++++++++++++
 .../shiro/web/RestoreSystemProperties.java    | 69 +++++++++++++++++++
 5 files changed, 182 insertions(+), 13 deletions(-)
 create mode 100644 web/src/test/java/org/apache/shiro/web/RestoreSystemProperties.java

--- a/web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
+++ b/web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
@@ -19,10 +19,12 @@
 
 package org.apache.shiro.web.filter;
 
+import org.apache.shiro.util.StringUtils;
 import org.apache.shiro.web.util.WebUtils;
 
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -48,16 +50,24 @@
 
     private boolean blockSemicolon = true;
 
-    private boolean blockBackslash = true;
+    private boolean blockBackslash = !Boolean.getBoolean(WebUtils.ALLOW_BACKSLASH);
 
     private boolean blockNonAscii = true;
 
     @Override
-    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
-        String uri = WebUtils.toHttp(request).getRequestURI();
-        return !containsSemicolon(uri)
-            && !containsBackslash(uri)
-            && !containsNonAsciiCharacters(uri);
+    protected boolean isAccessAllowed(ServletRequest req, ServletResponse response, Object mappedValue) throws Exception {
+        HttpServletRequest request = WebUtils.toHttp(req);
+        // check the original and decoded values
+        return isValid(request.getRequestURI())      // user request string (not decoded)
+                && isValid(request.getServletPath()) // decoded servlet part
+                && isValid(request.getPathInfo());   // decoded path info (may be null)
+    }
+
+    private boolean isValid(String uri) {
+        return !StringUtils.hasText(uri)
+               || ( !containsSemicolon(uri)
+                 && !containsBackslash(uri)
+                 && !containsNonAsciiCharacters(uri));
     }
 
     @Override
--- a/web/src/main/java/org/apache/shiro/web/util/WebUtils.java
+++ b/web/src/main/java/org/apache/shiro/web/util/WebUtils.java
@@ -56,6 +56,8 @@
     public static final String SERVLET_REQUEST_KEY = ServletRequest.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
     public static final String SERVLET_RESPONSE_KEY = ServletResponse.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
 
+    public static final String ALLOW_BACKSLASH = "org.apache.shiro.web.ALLOW_BACKSLASH";
+
     /**
      * {@link org.apache.shiro.session.Session Session} key used to save a request and later restore it, for example when redirecting to a
      * requested page after login, equal to {@code shiroSavedRequest}.
@@ -162,7 +164,7 @@
      * @return normalized path
      */
     public static String normalize(String path) {
-        return normalize(path, true);
+        return normalize(path, Boolean.getBoolean(ALLOW_BACKSLASH));
     }
 
     /**
--- a/web/src/test/groovy/org/apache/shiro/web/util/WebUtilsTest.groovy
+++ b/web/src/test/groovy/org/apache/shiro/web/util/WebUtilsTest.groovy
@@ -18,12 +18,15 @@
  */
 package org.apache.shiro.web.util
 
+import org.apache.shiro.web.RestoreSystemProperties
+import org.hamcrest.CoreMatchers
 import org.junit.Test
 
 import javax.servlet.http.HttpServletRequest
 
 import static org.easymock.EasyMock.*
 import static org.junit.Assert.*
+import static org.hamcrest.CoreMatchers.*
 
 /**
  * Tests for {@link WebUtils}.
@@ -162,6 +165,55 @@
 
     }
 
+    @Test
+    void testNormalize() {
+        doNormalizeTest"/foobar", "/foobar"
+        doNormalizeTest "/foobar/", "/foobar/"
+        doNormalizeTest"", "/"
+        doNormalizeTest"foobar", "/foobar"
+        doNormalizeTest"//foobar", "/foobar"
+        doNormalizeTest"//foobar///", "/foobar/"
+        doNormalizeTest"/context-path/foobar", "/context-path/foobar"
+        doNormalizeTest"/context-path/foobar/", "/context-path/foobar/"
+        doNormalizeTest"//context-path/foobar", "/context-path/foobar"
+        doNormalizeTest"//context-path//foobar" ,"/context-path/foobar"
+        doNormalizeTest"//context-path/remove-one/remove-two/../../././/foobar", "/context-path/foobar"
+        doNormalizeTest"//context-path//../../././/foobar", null
+        doNormalizeTest"/context path/foobar", "/context path/foobar"
+
+        doNormalizeTest"/context path/\\foobar", "/context path/\\foobar"
+        doNormalizeTest"//context-path\\..\\../.\\.\\foobar", "/context-path\\..\\../.\\.\\foobar"
+        doNormalizeTest"//context-path\\..\\..\\.\\.\\foobar", "/context-path\\..\\..\\.\\.\\foobar"
+        doNormalizeTest"\\context-path\\..\\foobar", "/\\context-path\\..\\foobar"
+    }
+
+    @Test
+    void testNormalize_allowBackslashes() {
+        RestoreSystemProperties.withProperties(["org.apache.shiro.web.ALLOW_BACKSLASH": "true"]) {
+            doNormalizeTest"/foobar", "/foobar"
+            doNormalizeTest "/foobar/", "/foobar/"
+            doNormalizeTest"", "/"
+            doNormalizeTest"foobar", "/foobar"
+            doNormalizeTest"//foobar", "/foobar"
+            doNormalizeTest"//foobar///", "/foobar/"
+            doNormalizeTest"/context-path/foobar", "/context-path/foobar"
+            doNormalizeTest"/context-path/foobar/", "/context-path/foobar/"
+            doNormalizeTest"//context-path/foobar", "/context-path/foobar"
+            doNormalizeTest"//context-path//foobar" ,"/context-path/foobar"
+            doNormalizeTest"//context-path/remove-one/remove-two/../../././/foobar", "/context-path/foobar"
+            doNormalizeTest"//context-path//../../././/foobar", null
+            doNormalizeTest"/context path/foobar", "/context path/foobar"
+            doNormalizeTest"/context path/\\foobar", "/context path/foobar"
+            doNormalizeTest"//context-path\\..\\..\\.\\.\\foobar", null
+            doNormalizeTest"\\context-path\\..\\foobar", "/foobar"
+
+        }
+    }
+
+    void doNormalizeTest(String path, String expected) {
+        assertThat WebUtils.normalize(path), equalTo(expected)
+    }
+
     void doTestGetPathWithinApplication(String contextPath, String requestUri, String expectedValue) {
 
         def request = createMock(HttpServletRequest)
--- /dev/null
+++ b/web/src/test/java/org/apache/shiro/web/RestoreSystemProperties.java
@@ -0,0 +1,69 @@
+/*
+ * 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.web;
+
+import groovy.lang.Closure;
+
+import java.io.Closeable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Wrapper that will restore System properties after test methods.
+ *
+ * Based on: https://github.com/stefanbirkner/system-rules/blob/master/src/main/java/org/junit/contrib/java/lang/system/RestoreSystemProperties.java
+ */
+public class RestoreSystemProperties implements Closeable {
+
+    private final Properties originalProperties;
+
+    public RestoreSystemProperties() {
+        originalProperties = System.getProperties();
+        System.setProperties(copyOf(originalProperties));
+    }
+
+    public void restore() {
+        System.setProperties(originalProperties);
+    }
+
+    private Properties copyOf(Properties source) {
+        Properties copy = new Properties();
+        copy.putAll(source);
+        return copy;
+    }
+
+    public static <T> T withProperties(Closure<T> closure) {
+        return withProperties(Collections.emptyMap(), closure);
+    }
+
+    public static <T> T withProperties(Map<String, String> properties, Closure<T> closure) {
+
+        try (RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties()) {
+            properties.forEach(System::setProperty);
+
+            return closure.call();
+        }
+    }
+
+    @Override
+    public void close() {
+        restore();
+    }
+}
>From 74d4cb6aee9aa1af4b098edc526a1e5630743f9b Mon Sep 17 00:00:00 2001
From: Brian Demers <[email protected]>
Date: Tue, 29 Sep 2020 17:59:29 -0400
Subject: [PATCH] Disable jsessionid URL rewriting by default

This matches the default of the InvalidRequestFilter

Fixes: SHIRO-795
---
 .../spring/web/config/AbstractShiroWebConfiguration.java     | 2 +-
 .../shiro/web/session/mgt/DefaultWebSessionManager.java      | 2 +-
 .../web/session/mgt/DefaultWebSessionManagerTest.groovy      | 5 ++++-
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/web/src/main/java/org/apache/shiro/web/session/mgt/DefaultWebSessionManager.java b/web/src/main/java/org/apache/shiro/web/session/mgt/DefaultWebSessionManager.java
index eb7eda1f..9aa275a9 100644
--- a/web/src/main/java/org/apache/shiro/web/session/mgt/DefaultWebSessionManager.java
+++ b/web/src/main/java/org/apache/shiro/web/session/mgt/DefaultWebSessionManager.java
@@ -58,7 +58,7 @@ public DefaultWebSessionManager() {
         cookie.setHttpOnly(true); //more secure, protects against XSS attacks
         this.sessionIdCookie = cookie;
         this.sessionIdCookieEnabled = true;
-        this.sessionIdUrlRewritingEnabled = true;
+        this.sessionIdUrlRewritingEnabled = false;
     }
 
     public Cookie getSessionIdCookie() {
diff --git a/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy b/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy
index 841569fc..35b31204 100644
--- a/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy
+++ b/web/src/test/groovy/org/apache/shiro/web/session/mgt/DefaultWebSessionManagerTest.groovy
@@ -127,7 +127,7 @@ public class DefaultWebSessionManagerTest {
                 ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
         request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
         request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
-        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, Boolean.TRUE);
+        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, Boolean.FALSE);
 
         replay(cookie);
         replay(request);
@@ -147,6 +147,7 @@ public class DefaultWebSessionManagerTest {
         Cookie cookie = createMock(Cookie.class);
         mgr.setSessionIdCookie(cookie);
         mgr.setSessionIdCookieEnabled(false);
+        mgr.setSessionIdUrlRewritingEnabled(true)
 
         //we should not have any reads from the cookie fields - if we do, this test case will fail.
 
@@ -182,6 +183,7 @@ public class DefaultWebSessionManagerTest {
         Cookie cookie = createMock(Cookie.class);
         mgr.setSessionIdCookie(cookie);
         mgr.setSessionIdCookieEnabled(false);
+        mgr.setSessionIdUrlRewritingEnabled(true)
 
         //we should not have any reads from the cookie fields - if we do, this test case will fail.
 
@@ -218,6 +220,7 @@ public class DefaultWebSessionManagerTest {
     public void testGetSessionIdFromRequestUriPathSegmentParam() {
 
         mgr.setSessionIdCookieEnabled(false);
+        mgr.setSessionIdUrlRewritingEnabled(true)
 
         HttpServletRequest request = createMock(HttpServletRequest.class);
         HttpServletResponse response = createMock(HttpServletResponse.class);
-- 
2.20.1

Reply via email to