Add a feature to allow for global filters
Adds new filter to block invalid requests
diff --git 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
index df95665..73ffc7b 100644
--- 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
@@ -30,6 +30,7 @@
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.util.StringUtils;
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;
@@ -87,7 +88,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";
@@ -124,6 +126,10 @@
};
}
+ public List<FilterConfig<? extends Filter>> globalFilters() {
+ return Collections.singletonList(filterConfig(INVALID_REQUEST));
+ }
+
@Override
protected final void configureShiro() {
bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
@@ -135,6 +141,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 FilterConfig[0]);
+ }
+
bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(setupFilterChainConfigs()));
}
@@ -153,8 +165,15 @@
// collect the keys used for this path
List<Key<? extends Filter>> keysForPath = new ArrayList<Key<? extends Filter>>();
- for (int i = 0; i < filterChain.getValue().length; i++) {
- FilterConfig<? extends Filter> filterConfig = filterChain.getValue()[i];
+ List<FilterConfig<? extends Filter>> globalFilters = this.globalFilters();
+ FilterConfig<? extends Filter>[] pathFilters = filterChain.getValue();
+
+ // merge the global filters and the path specific filters
+ List<FilterConfig<? extends Filter>> filterConfigs = new ArrayList<>(globalFilters.size() + pathFilters.length);
+ filterConfigs.addAll(globalFilters);
+ filterConfigs.addAll(Arrays.asList(pathFilters));
+
+ for (FilterConfig<? extends Filter> filterConfig : filterConfigs) {
Key<? extends Filter> key = filterConfig.getKey();
String config = filterConfig.getConfigValue();
diff --git a/support/guice/src/test/java/org/apache/shiro/guice/web/ShiroWebModuleTest.java b/support/guice/src/test/java/org/apache/shiro/guice/web/ShiroWebModuleTest.java
index 9931d01..6de5a75 100644
--- a/support/guice/src/test/java/org/apache/shiro/guice/web/ShiroWebModuleTest.java
+++ b/support/guice/src/test/java/org/apache/shiro/guice/web/ShiroWebModuleTest.java
@@ -24,6 +24,7 @@
import com.google.inject.Key;
import com.google.inject.Provides;
import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.name.Names;
import org.apache.shiro.guice.ShiroModuleTest;
import org.apache.shiro.env.Environment;
import org.apache.shiro.mgt.SecurityManager;
@@ -31,6 +32,7 @@
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.web.env.EnvironmentLoader;
import org.apache.shiro.web.env.WebEnvironment;
+import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
@@ -55,7 +57,9 @@
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;
@@ -212,29 +216,38 @@
FilterChain filterChain = simpleFilterChainResolver.getChain(request, null, null);
assertThat(filterChain, instanceOf(SimpleFilterChain.class));
Filter nextFilter = getNextFilter((SimpleFilterChain) filterChain);
+ assertThat(nextFilter, instanceOf(InvalidRequestFilter.class));
+ nextFilter = getNextFilter((SimpleFilterChain) filterChain);
assertThat(nextFilter, instanceOf(FormAuthenticationFilter.class));
// test the /test_custom_filter resource
filterChain = simpleFilterChainResolver.getChain(request, null, null);
assertThat(filterChain, instanceOf(SimpleFilterChain.class));
nextFilter = getNextFilter((SimpleFilterChain) filterChain);
+ assertThat(nextFilter, instanceOf(InvalidRequestFilter.class));
+ nextFilter = getNextFilter((SimpleFilterChain) filterChain);
assertThat(nextFilter, instanceOf(CustomFilter.class));
// test the /test_authc_basic resource
filterChain = simpleFilterChainResolver.getChain(request, null, null);
assertThat(filterChain, instanceOf(SimpleFilterChain.class));
nextFilter = getNextFilter((SimpleFilterChain) filterChain);
+ assertThat(nextFilter, instanceOf(InvalidRequestFilter.class));
+ nextFilter = getNextFilter((SimpleFilterChain) filterChain);
assertThat(nextFilter, instanceOf(BasicHttpAuthenticationFilter.class));
// test the /test_perms resource
filterChain = simpleFilterChainResolver.getChain(request, null, null);
assertThat(filterChain, instanceOf(SimpleFilterChain.class));
nextFilter = getNextFilter((SimpleFilterChain) filterChain);
+ assertThat(nextFilter, instanceOf(InvalidRequestFilter.class));
+ nextFilter = getNextFilter((SimpleFilterChain) filterChain);
assertThat(nextFilter, instanceOf(PermissionsAuthorizationFilter.class));
// test the /multiple_configs resource
filterChain = simpleFilterChainResolver.getChain(request, null, null);
assertThat(filterChain, instanceOf(SimpleFilterChain.class));
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(InvalidRequestFilter.class));
assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(FormAuthenticationFilter.class));
assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(RolesAuthorizationFilter.class));
assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(PermissionsAuthorizationFilter.class));
@@ -312,6 +325,146 @@
verify(servletContext, request);
}
+ @Test
+ public void testDefaultPath() {
+
+ final ShiroModuleTest.MockRealm mockRealm = createMock(ShiroModuleTest.MockRealm.class);
+ ServletContext servletContext = createMock(ServletContext.class);
+ HttpServletRequest request = createMock(HttpServletRequest.class);
+
+ servletContext.setAttribute(eq(EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY), EasyMock.anyObject());
+ expect(request.getAttribute("javax.servlet.include.context_path")).andReturn("").anyTimes();
+ expect(request.getCharacterEncoding()).andReturn("UTF-8").anyTimes();
+ expect(request.getAttribute("javax.servlet.include.path_info")).andReturn(null).anyTimes();
+ expect(request.getPathInfo()).andReturn(null).anyTimes();
+ expect(request.getAttribute("javax.servlet.include.servlet_path")).andReturn("/test/foobar");
+ replay(servletContext, request);
+
+ Injector injector = Guice.createInjector(new ShiroWebModule(servletContext) {
+ @Override
+ protected void configureShiroWeb() {
+ bindRealm().to(ShiroModuleTest.MockRealm.class);
+ expose(FilterChainResolver.class);
+ // no paths configured
+ }
+
+ @Provides
+ public ShiroModuleTest.MockRealm createRealm() {
+ return mockRealm;
+ }
+ });
+
+ FilterChainResolver resolver = injector.getInstance(FilterChainResolver.class);
+ assertThat(resolver, instanceOf(SimpleFilterChainResolver.class));
+ SimpleFilterChainResolver simpleFilterChainResolver = (SimpleFilterChainResolver) resolver;
+
+ // test the /test_authc resource
+ FilterChain filterChain = simpleFilterChainResolver.getChain(request, null, null);
+ assertThat(filterChain, instanceOf(SimpleFilterChain.class));
+
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(InvalidRequestFilter.class));
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), nullValue());
+
+ verify(servletContext, request);
+ }
+
+ @Test
+ public void testDisableGlobalFilters() {
+
+ final ShiroModuleTest.MockRealm mockRealm = createMock(ShiroModuleTest.MockRealm.class);
+ ServletContext servletContext = createMock(ServletContext.class);
+ HttpServletRequest request = createMock(HttpServletRequest.class);
+
+ servletContext.setAttribute(eq(EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY), EasyMock.anyObject());
+ expect(request.getAttribute("javax.servlet.include.context_path")).andReturn("").anyTimes();
+ expect(request.getCharacterEncoding()).andReturn("UTF-8").anyTimes();
+ expect(request.getAttribute("javax.servlet.include.path_info")).andReturn(null).anyTimes();
+ expect(request.getPathInfo()).andReturn(null).anyTimes();
+ expect(request.getAttribute("javax.servlet.include.servlet_path")).andReturn("/test/foobar");
+ replay(servletContext, request);
+
+ Injector injector = Guice.createInjector(new ShiroWebModule(servletContext) {
+ @Override
+ protected void configureShiroWeb() {
+ bindRealm().to(ShiroModuleTest.MockRealm.class);
+ expose(FilterChainResolver.class);
+ this.addFilterChain("/**", filterConfig(AUTHC));
+ }
+
+ @Override
+ public List<FilterConfig<? extends Filter>> globalFilters() {
+ return Collections.emptyList();
+ }
+
+ @Provides
+ public ShiroModuleTest.MockRealm createRealm() {
+ return mockRealm;
+ }
+ });
+
+ FilterChainResolver resolver = injector.getInstance(FilterChainResolver.class);
+ assertThat(resolver, instanceOf(SimpleFilterChainResolver.class));
+ SimpleFilterChainResolver simpleFilterChainResolver = (SimpleFilterChainResolver) resolver;
+
+ // test the /test_authc resource
+ FilterChain filterChain = simpleFilterChainResolver.getChain(request, null, null);
+ assertThat(filterChain, instanceOf(SimpleFilterChain.class));
+
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(FormAuthenticationFilter.class));
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), nullValue());
+
+ verify(servletContext, request);
+ }
+
+ @Test
+ public void testChangeInvalidFilterConfig() {
+
+ final ShiroModuleTest.MockRealm mockRealm = createMock(ShiroModuleTest.MockRealm.class);
+ ServletContext servletContext = createMock(ServletContext.class);
+ HttpServletRequest request = createMock(HttpServletRequest.class);
+
+ servletContext.setAttribute(eq(EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY), EasyMock.anyObject());
+ expect(request.getAttribute("javax.servlet.include.context_path")).andReturn("").anyTimes();
+ expect(request.getCharacterEncoding()).andReturn("UTF-8").anyTimes();
+ expect(request.getAttribute("javax.servlet.include.path_info")).andReturn(null).anyTimes();
+ expect(request.getPathInfo()).andReturn(null).anyTimes();
+ expect(request.getAttribute("javax.servlet.include.servlet_path")).andReturn("/test/foobar");
+ replay(servletContext, request);
+
+ Injector injector = Guice.createInjector(new ShiroWebModule(servletContext) {
+ @Override
+ protected void configureShiroWeb() {
+
+ bindConstant().annotatedWith(Names.named("shiro.blockBackslash")).to(false);
+
+ bindRealm().to(ShiroModuleTest.MockRealm.class);
+ expose(FilterChainResolver.class);
+ this.addFilterChain("/**", filterConfig(AUTHC));
+ }
+
+ @Provides
+ public ShiroModuleTest.MockRealm createRealm() {
+ return mockRealm;
+ }
+ });
+
+ FilterChainResolver resolver = injector.getInstance(FilterChainResolver.class);
+ assertThat(resolver, instanceOf(SimpleFilterChainResolver.class));
+ SimpleFilterChainResolver simpleFilterChainResolver = (SimpleFilterChainResolver) resolver;
+
+ // test the /test_authc resource
+ FilterChain filterChain = simpleFilterChainResolver.getChain(request, null, null);
+ assertThat(filterChain, instanceOf(SimpleFilterChain.class));
+
+ Filter invalidRequestFilter = getNextFilter((SimpleFilterChain) filterChain);
+ assertThat(invalidRequestFilter, instanceOf(InvalidRequestFilter.class));
+ assertFalse("Expected 'blockBackslash' to be false", ((InvalidRequestFilter) invalidRequestFilter).isBlockBackslash());
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), instanceOf(FormAuthenticationFilter.class));
+ assertThat(getNextFilter((SimpleFilterChain) filterChain), nullValue());
+
+ verify(servletContext, request);
+ }
+
private Filter getNextFilter(SimpleFilterChain filterChain) {
Iterator<? extends Filter> filters = filterChain.getFilters();
diff --git a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java b/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
index 69356d6..9a60a0c 100644
--- a/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
+++ b/support/spring-boot/spring-boot-web-starter/src/main/java/org/apache/shiro/spring/config/web/autoconfigure/ShiroWebFilterConfiguration.java
@@ -28,6 +28,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import java.util.List;
+
/**
* @since 1.4.0
*/
@@ -53,4 +55,10 @@
return filterRegistrationBean;
}
+
+ @Bean(name = "globalFilters")
+ @ConditionalOnMissingBean
+ protected List<String> globalFilters() {
+ return super.globalFilters();
+ }
}
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy
new file mode 100644
index 0000000..34c89a4
--- /dev/null
+++ b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ConfiguredGlobalFiltersTest.groovy
@@ -0,0 +1,104 @@
+/*
+ * 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.spring.boot.autoconfigure.web
+
+import org.apache.shiro.spring.boot.autoconfigure.web.application.ShiroWebAutoConfigurationTestApplication
+import org.apache.shiro.spring.web.ShiroFilterFactoryBean
+import org.apache.shiro.spring.web.config.AbstractShiroWebFilterConfiguration
+import org.apache.shiro.web.filter.InvalidRequestFilter
+import org.apache.shiro.web.filter.authz.PortFilter
+import org.apache.shiro.web.filter.mgt.DefaultFilter
+import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager
+import org.apache.shiro.web.filter.mgt.NamedFilterList
+import org.apache.shiro.web.servlet.AbstractShiroFilter
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.junit4.SpringRunner
+
+import static org.hamcrest.MatcherAssert.assertThat
+import static org.hamcrest.Matchers.*
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = [ShiroWebAutoConfigurationTestApplication, Config])
+
+class ConfiguredGlobalFiltersTest {
+
+ @Configuration
+ static class Config extends AbstractShiroWebFilterConfiguration {
+
+ @Bean
+ List<String> globalFilters() {
+ return [DefaultFilter.invalidRequest.name(), DefaultFilter.port.name()]
+ }
+
+ @Bean
+ @Override
+ ShiroFilterFactoryBean shiroFilterFactoryBean() {
+ ShiroFilterFactoryBean bean = super.shiroFilterFactoryBean()
+ InvalidRequestFilter invalidRequestFilter = new InvalidRequestFilter()
+ invalidRequestFilter.setBlockBackslash(false)
+ PortFilter portFilter = new PortFilter()
+ portFilter.setPort(9999)
+ bean.getFilters().put("invalidRequest", invalidRequestFilter)
+ bean.getFilters().put("port", portFilter)
+ return bean
+ }
+ }
+
+ @Autowired
+ private AbstractShiroFilter shiroFilter
+
+ @Test
+ void testGlobalFiltersConfigured() {
+ // make sure global chains are configured
+ assertThat shiroFilter.filterChainResolver.filterChainManager, instanceOf(DefaultFilterChainManager)
+ DefaultFilterChainManager filterChainManager = shiroFilter.filterChainResolver.filterChainManager
+
+ // default config set
+ assertThat filterChainManager.globalFilterNames, contains(DefaultFilter.invalidRequest.name(),
+ DefaultFilter.port.name())
+ // default route configured
+ NamedFilterList allChain = filterChainManager.getChain("/**")
+ assertThat allChain, contains(
+ instanceOf(DefaultFilter.invalidRequest.filterClass),
+ instanceOf(DefaultFilter.port.filterClass))
+
+ InvalidRequestFilter invalidRequest = allChain.get(0)
+ assertThat "Expected invalidRequest.blockBackslash to be false", !invalidRequest.isBlockBackslash()
+ PortFilter portFilter = allChain.get(1) // an ugly line, but we want to make sure that we can override the filters
+ // defined in Shiro's DefaultFilter
+ assertThat portFilter.port, equalTo(9999)
+
+ // configured routes also contain global filters
+ NamedFilterList loginChain = filterChainManager.getChain("/login.html")
+ assertThat loginChain, contains(
+ instanceOf(DefaultFilter.invalidRequest.filterClass),
+ instanceOf(DefaultFilter.port.filterClass),
+ instanceOf(DefaultFilter.authc.filterClass)) // configured in ShiroWebAutoConfigurationTestApplication
+
+ assertThat loginChain.get(0), sameInstance(invalidRequest)
+ assertThat loginChain.get(1), sameInstance(portFilter)
+
+
+ }
+}
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy
new file mode 100644
index 0000000..983b666
--- /dev/null
+++ b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/DisabledGlobalFiltersTest.groovy
@@ -0,0 +1,64 @@
+/*
+ * 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.spring.boot.autoconfigure.web;
+
+import org.apache.shiro.spring.boot.autoconfigure.web.application.ShiroWebAutoConfigurationTestApplication
+import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager
+import org.apache.shiro.web.servlet.AbstractShiroFilter
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.junit4.SpringRunner
+
+import static org.hamcrest.MatcherAssert.assertThat
+import static org.hamcrest.Matchers.equalTo
+import static org.hamcrest.Matchers.instanceOf
+import static org.hamcrest.Matchers.nullValue
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = [ShiroWebAutoConfigurationTestApplication, Config])
+class DisabledGlobalFiltersTest {
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ List<String> globalFilters() {
+ return []
+ }
+ }
+
+ @Autowired
+ private AbstractShiroFilter shiroFilter
+
+ @Test
+ void testGlobalFiltersDisabled() {
+ // make sure global chains are configured
+ assertThat shiroFilter.filterChainResolver.filterChainManager, instanceOf(DefaultFilterChainManager)
+ DefaultFilterChainManager filterChainManager = shiroFilter.filterChainResolver.filterChainManager
+
+ // default config set
+ assertThat filterChainManager.globalFilterNames, equalTo([])
+ // default route configured
+ assertThat filterChainManager.getChain("/**"), nullValue()
+ }
+}
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy
index e69d052..ad19589 100644
--- a/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy
+++ b/support/spring-boot/spring-boot-web-starter/src/test/groovy/org/apache/shiro/spring/boot/autoconfigure/web/ShiroWebSpringAutoConfigurationTest.groovy
@@ -25,20 +25,26 @@
import org.apache.shiro.event.EventBus
import org.apache.shiro.mgt.DefaultSecurityManager
import org.apache.shiro.mgt.SecurityManager
+import org.apache.shiro.web.filter.mgt.DefaultFilter
+import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager
import org.apache.shiro.web.mgt.WebSecurityManager
+import org.apache.shiro.web.servlet.AbstractShiroFilter
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests
-import static org.hamcrest.MatcherAssert.assertThat
+import static org.hamcrest.Matchers.contains
+import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.instanceOf
import static org.junit.Assert.*
+import static org.hamcrest.MatcherAssert.assertThat
+
/**
* @since 1.4.0
*/
@SpringBootTest(classes = [ShiroWebAutoConfigurationTestApplication])
-public class ShiroWebSpringAutoConfigurationTest extends AbstractJUnit4SpringContextTests {
+class ShiroWebSpringAutoConfigurationTest extends AbstractJUnit4SpringContextTests {
@Autowired
private SecurityManager securityManager
@@ -52,8 +58,11 @@
@Autowired
private SubscribedListener subscribedListener
+ @Autowired
+ private AbstractShiroFilter shiroFilter
+
@Test
- public void testMinimalConfiguration() {
+ void testMinimalConfiguration() {
// first do a quick check of the injected objects
assertNotNull securityManager
@@ -61,11 +70,22 @@
assertNotNull eventBusAwareObject
assertNotNull eventBus
+ assertNotNull shiroFilter
assertTrue(eventBus.registry.containsKey(subscribedListener))
assertSame(eventBusAwareObject.getEventBus(), eventBus)
assertSame(((DefaultSecurityManager)securityManager).getEventBus(), eventBus)
+ // make sure global chains are configured
+ assertThat shiroFilter.filterChainResolver.filterChainManager, instanceOf(DefaultFilterChainManager)
+ DefaultFilterChainManager filterChainManager = shiroFilter.filterChainResolver.filterChainManager
+
+ // default config set
+ assertThat filterChainManager.globalFilterNames, equalTo([DefaultFilter.invalidRequest.name()])
+ // default route configured
+ assertThat filterChainManager.getChain("/**"), contains(instanceOf(DefaultFilter.invalidRequest.filterClass))
+ // configured routes also contain global filters
+ assertThat filterChainManager.getChain("/login.html"), contains(
+ instanceOf(DefaultFilter.invalidRequest.filterClass),
+ instanceOf(DefaultFilter.authc.filterClass)) // configured in ShiroWebAutoConfigurationTestApplication
}
-
-
}
diff --git a/support/spring-boot/spring-boot-web-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java b/support/spring-boot/spring-boot-web-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java
index c1fdc43..8e778bb 100644
--- a/support/spring-boot/spring-boot-web-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java
+++ b/support/spring-boot/spring-boot-web-starter/src/test/java/org/apache/shiro/spring/boot/autoconfigure/web/application/ShiroWebAutoConfigurationTestApplication.java
@@ -55,7 +55,9 @@
@Bean
ShiroFilterChainDefinition shiroFilterChainDefinition() {
- return new DefaultShiroFilterChainDefinition();
+ DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
+ chainDefinition.addPathDefinition("/login.html", "authc");
+ return chainDefinition;
}
@Bean
diff --git 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
index 5f006a1..1065a63 100644
--- 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<>();
+ 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;
}
@@ -533,6 +555,7 @@
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
}
setSecurityManager(webSecurityManager);
+
if (resolver != null) {
setFilterChainResolver(resolver);
}
diff --git a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java
index 6e2cad3..414d7b1 100644
--- a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java
+++ b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebConfiguration.java
@@ -18,13 +18,10 @@
*/
package org.apache.shiro.spring.web.config;
-import org.apache.shiro.config.Ini;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.mgt.SubjectFactory;
-import org.apache.shiro.realm.Realm;
-import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.config.AbstractShiroConfiguration;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
diff --git a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
index e15d50d..11b961e 100644
--- a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
+++ b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
@@ -18,13 +18,15 @@
*/
package org.apache.shiro.spring.web.config;
-
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
+import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.Filter;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
/**
@@ -50,6 +52,10 @@
@Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")
protected String unauthorizedUrl;
+ protected List<String> globalFilters() {
+ return Collections.singletonList(DefaultFilter.invalidRequest.name());
+ }
+
protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
@@ -58,6 +64,7 @@
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
filterFactoryBean.setSecurityManager(securityManager);
+ filterFactoryBean.setGlobalFilters(globalFilters());
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
filterFactoryBean.setFilters(filterMap);
diff --git a/support/spring/src/main/java/org/apache/shiro/spring/web/config/ShiroWebFilterConfiguration.java b/support/spring/src/main/java/org/apache/shiro/spring/web/config/ShiroWebFilterConfiguration.java
index 72a4ecb..0ddcab1 100644
--- a/support/spring/src/main/java/org/apache/shiro/spring/web/config/ShiroWebFilterConfiguration.java
+++ b/support/spring/src/main/java/org/apache/shiro/spring/web/config/ShiroWebFilterConfiguration.java
@@ -22,6 +22,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import java.util.List;
+
/**
* @since 1.4.0
*/
@@ -34,4 +36,8 @@
return super.shiroFilterFactoryBean();
}
+ @Bean(name = "globalFilters")
+ protected List<String> globalFilters() {
+ return super.globalFilters();
+ }
}
diff --git a/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy b/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
index 68870dd..bd7e25c 100644
--- a/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
+++ b/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
@@ -24,6 +24,7 @@
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.mgt.FilterChainManager
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
@@ -66,7 +67,7 @@
// create the filter chain manager
FilterChainManager filterChainManager = shiroFilterFactoryBean.createFilterChainManager()
// lookup the chain by name
- assertThat filterChainManager.getChain("/test-me"), contains(instanceOf(ExpectedTestFilter))
+ assertThat filterChainManager.getChain("/test-me"), contains(instanceOf(InvalidRequestFilter), instanceOf(ExpectedTestFilter))
}
@Configuration
diff --git a/support/spring/src/test/java/org/apache/shiro/spring/web/ShiroFilterFactoryBeanTest.java b/support/spring/src/test/java/org/apache/shiro/spring/web/ShiroFilterFactoryBeanTest.java
index b2ba586..603d1c4 100644
--- a/support/spring/src/test/java/org/apache/shiro/spring/web/ShiroFilterFactoryBeanTest.java
+++ b/support/spring/src/test/java/org/apache/shiro/spring/web/ShiroFilterFactoryBeanTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.shiro.spring.web;
+import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.filter.mgt.NamedFilterList;
@@ -55,11 +56,12 @@
DefaultFilterChainManager fcManager = (DefaultFilterChainManager) resolver.getFilterChainManager();
NamedFilterList chain = fcManager.getChain("/test");
assertNotNull(chain);
- assertEquals(chain.size(), 2);
+ assertEquals(chain.size(), 3);
Filter[] filters = new Filter[chain.size()];
filters = chain.toArray(filters);
- assertTrue(filters[0] instanceof DummyFilter);
- assertTrue(filters[1] instanceof FormAuthenticationFilter);
+ assertTrue(filters[0] instanceof InvalidRequestFilter); // global filter
+ assertTrue(filters[1] instanceof DummyFilter);
+ assertTrue(filters[2] instanceof FormAuthenticationFilter);
}
/**
diff --git a/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java b/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
index fb9f822..7118164 100644
--- 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());
+
public IniFilterChainResolverFactory() {
super();
}
@@ -70,6 +75,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) {
@@ -121,9 +134,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) {
diff --git a/web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java b/web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
new file mode 100644
index 0000000..3d229e9
--- /dev/null
+++ b/web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java
@@ -0,0 +1,124 @@
+/*
+ * 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()) {
+ return SEMICOLON.stream().anyMatch(uri::contains);
+ }
+ return false;
+ }
+
+ private boolean containsBackslash(String uri) {
+ if (isBlockBackslash()) {
+ return BACKSLASH.stream().anyMatch(uri::contains);
+ }
+ 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;
+ }
+}
diff --git 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
index 036f62f..be77f06 100644
--- 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;
@@ -48,7 +49,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;
diff --git 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
index a880108..f289a79 100644
--- 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<>();
addDefaultFilters(false);
}
public DefaultFilterChainManager(FilterConfig filterConfig) {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
+ this.globalFilterNames = new ArrayList<>();
setFilterConfig(filterConfig);
addDefaultFilters(true);
}
@@ -115,6 +121,15 @@
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
+ globalFilterNames.stream().forEach(filterName -> 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 +139,12 @@
}
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)) {
+ globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName));
}
//parse the value by tokenizing it to get the resulting filter-specific config entries
@@ -273,6 +293,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 + "] " +
diff --git 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
index ebbc716..268c723 100644
--- 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;
}
diff --git a/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java b/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java
index 8f0d5bb..7e2ed55 100644
--- a/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java
+++ b/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java
@@ -404,6 +404,7 @@
* @since 1.0
*/
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
+
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
diff --git a/web/src/test/groovy/org/apache/shiro/web/config/IniFilterChainResolverFactoryTest.groovy b/web/src/test/groovy/org/apache/shiro/web/config/IniFilterChainResolverFactoryTest.groovy
index 6a4e276..ba9c57f 100644
--- a/web/src/test/groovy/org/apache/shiro/web/config/IniFilterChainResolverFactoryTest.groovy
+++ b/web/src/test/groovy/org/apache/shiro/web/config/IniFilterChainResolverFactoryTest.groovy
@@ -18,6 +18,10 @@
*/
package org.apache.shiro.web.config
+import org.apache.shiro.web.filter.InvalidRequestFilter
+import org.apache.shiro.web.filter.mgt.DefaultFilter
+import org.hamcrest.Matchers
+
import javax.servlet.Filter
import javax.servlet.FilterConfig
import javax.servlet.ServletContext
@@ -30,6 +34,7 @@
import static org.easymock.EasyMock.*
import static org.junit.Assert.*
+import static org.hamcrest.MatcherAssert.assertThat
/**
* Unit tests for the {@link IniFilterChainResolverFactory} implementation.
@@ -57,6 +62,7 @@
assertNull factory.filterConfig
factory.filterConfig = null
assertNull factory.filterConfig
+ assertThat factory.globalFilters, Matchers.contains(DefaultFilter.invalidRequest.name())
}
@Test
@@ -74,6 +80,7 @@
factory = new IniFilterChainResolverFactory(ini)
FilterChainResolver resolver = factory.getInstance()
assertNotNull resolver
+ assertThat resolver.filterChainManager.globalFilterNames, Matchers.contains(DefaultFilter.invalidRequest.name())
}
@Test
@@ -173,4 +180,23 @@
assertTrue extractedFilters['filtersSectionFilter'] instanceof SslFilter
assertTrue extractedFilters['mainSectionFilter'] instanceof FormAuthenticationFilter
}
+
+ @Test
+ void testConfigureInvalidRequestFilter() {
+ Ini ini = new Ini()
+ ini.load("""
+ [main]
+ invalidRequest.blockBackslash = false
+ [urls]
+ /index.html = anon
+ """)
+ factory = new IniFilterChainResolverFactory(ini)
+ FilterChainResolver resolver = factory.getInstance()
+ assertNotNull resolver
+
+ def invalidRequestFilter = resolver.filterChainManager.getChain("/index.html").get(0) // this will be the invalidRequest filter
+
+ assertThat(invalidRequestFilter, Matchers.instanceOf(InvalidRequestFilter))
+ assertThat("blockSemicolon should be faluse", invalidRequestFilter.blockBackslash)
+ }
}
diff --git a/web/src/test/groovy/org/apache/shiro/web/env/IniWebEnvironmentTest.groovy b/web/src/test/groovy/org/apache/shiro/web/env/IniWebEnvironmentTest.groovy
index 7dcb40b..cee3cfd 100644
--- a/web/src/test/groovy/org/apache/shiro/web/env/IniWebEnvironmentTest.groovy
+++ b/web/src/test/groovy/org/apache/shiro/web/env/IniWebEnvironmentTest.groovy
@@ -22,8 +22,11 @@
import org.apache.shiro.config.Ini
import org.apache.shiro.config.SimpleBean
import org.apache.shiro.web.filter.mgt.DefaultFilter
+import org.apache.shiro.web.filter.mgt.FilterChainManager
+import org.hamcrest.Matchers
import org.junit.Test
+import static org.hamcrest.MatcherAssert.assertThat
import static org.junit.Assert.*
/**
@@ -94,4 +97,70 @@
assertSame(compositeBean.simpleBean, simpleBean)
}
+
+ @Test
+ void testDisableGlobalFilters() {
+ Ini ini = new Ini()
+ ini.load("""
+ [main]
+ filterChainResolver.globalFilters = null
+
+ [urls]
+ /index.html = anon
+ """)
+
+ def env = new IniWebEnvironment(ini: ini)
+ env.init()
+ assertThat env.getFilterChainResolver().filterChainManager.globalFilterNames, Matchers.empty()
+ }
+
+ @Test
+ void testDefaultGlobalFilters() {
+ Ini ini = new Ini()
+ ini.load("""
+ [main]
+
+ [urls]
+ /index.html = anon
+ """)
+
+ def env = new IniWebEnvironment(ini: ini)
+ env.init()
+ def resolver = env.getFilterChainResolver()
+ FilterChainManager manager = resolver.filterChainManager
+ assertThat manager.globalFilterNames, Matchers.contains(DefaultFilter.invalidRequest.name())
+
+ assertThat manager.getChain("/index.html"), Matchers.contains(
+ Matchers.instanceOf(DefaultFilter.invalidRequest.filterClass),
+ Matchers.instanceOf(DefaultFilter.anon.filterClass))
+ }
+
+ @Test
+ void testCustomGlobalFilters() {
+ Ini ini = new Ini()
+ ini.load("""
+ [main]
+ stub = org.apache.shiro.web.env.FilterStub
+ filterChainResolver.globalFilters = port,invalidRequest,stub
+
+ [urls]
+ /index.html = authc
+ """)
+
+ def env = new IniWebEnvironment(ini: ini)
+ env.init()
+ def resolver = env.getFilterChainResolver()
+ FilterChainManager manager = resolver.filterChainManager
+ assertThat manager.globalFilterNames, Matchers.contains(
+ DefaultFilter.port.name(),
+ DefaultFilter.invalidRequest.name(),
+ "stub"
+ )
+
+ assertThat manager.getChain("/index.html"), Matchers.contains(
+ Matchers.instanceOf(DefaultFilter.port.filterClass),
+ Matchers.instanceOf(DefaultFilter.invalidRequest.filterClass),
+ Matchers.instanceOf(FilterStub),
+ Matchers.instanceOf(DefaultFilter.authc.filterClass))
+ }
}
diff --git a/web/src/test/groovy/org/apache/shiro/web/filter/InvalidRequestFilterTest.groovy b/web/src/test/groovy/org/apache/shiro/web/filter/InvalidRequestFilterTest.groovy
new file mode 100644
index 0000000..8d0b1c0
--- /dev/null
+++ b/web/src/test/groovy/org/apache/shiro/web/filter/InvalidRequestFilterTest.groovy
@@ -0,0 +1,106 @@
+/*
+ * 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.junit.Test
+
+import javax.servlet.http.HttpServletRequest
+
+import static org.easymock.EasyMock.expect
+import static org.easymock.EasyMock.mock
+import static org.easymock.EasyMock.replay
+import static org.hamcrest.MatcherAssert.assertThat
+
+class InvalidRequestFilterTest {
+
+ @Test
+ void defaultConfig() {
+ InvalidRequestFilter filter = new InvalidRequestFilter()
+ assertThat "filter.blockBackslash expected to be true", filter.isBlockBackslash()
+ assertThat "filter.blockNonAscii expected to be true", filter.isBlockNonAscii()
+ assertThat "filter.blockSemicolon expected to be true", filter.isBlockSemicolon()
+ }
+
+ @Test
+ void testFilterBlocks() {
+ InvalidRequestFilter filter = new InvalidRequestFilter()
+ assertPathBlocked(filter, "/\\something")
+ assertPathBlocked(filter, "/%5csomething")
+ assertPathBlocked(filter, "/%5Csomething")
+ assertPathBlocked(filter, "/;something")
+ assertPathBlocked(filter, "/%3bsomething")
+ assertPathBlocked(filter, "/%3Bsomething")
+ assertPathBlocked(filter, "/\u0019something")
+ }
+
+ @Test
+ void testFilterAllowsBackslash() {
+ InvalidRequestFilter filter = new InvalidRequestFilter()
+ filter.setBlockBackslash(false)
+ assertPathAllowed(filter, "/\\something")
+ assertPathAllowed(filter, "/%5csomething")
+ assertPathAllowed(filter, "/%5Csomething")
+ assertPathBlocked(filter, "/;something")
+ assertPathBlocked(filter, "/%3bsomething")
+ assertPathBlocked(filter, "/%3Bsomething")
+ assertPathBlocked(filter, "/\u0019something")
+ }
+
+ @Test
+ void testFilterAllowsNonAscii() {
+ InvalidRequestFilter filter = new InvalidRequestFilter()
+ filter.setBlockNonAscii(false)
+ assertPathBlocked(filter, "/\\something")
+ assertPathBlocked(filter, "/%5csomething")
+ assertPathBlocked(filter, "/%5Csomething")
+ assertPathBlocked(filter, "/;something")
+ assertPathBlocked(filter, "/%3bsomething")
+ assertPathBlocked(filter, "/%3Bsomething")
+ assertPathAllowed(filter, "/\u0019something")
+ }
+ @Test
+ void testFilterAllowsSemicolon() {
+ InvalidRequestFilter filter = new InvalidRequestFilter()
+ filter.setBlockSemicolon(false)
+ assertPathBlocked(filter, "/\\something")
+ assertPathBlocked(filter, "/%5csomething")
+ assertPathBlocked(filter, "/%5Csomething")
+ assertPathAllowed(filter, "/;something")
+ assertPathAllowed(filter, "/%3bsomething")
+ assertPathAllowed(filter, "/%3Bsomething")
+ assertPathBlocked(filter, "/\u0019something")
+ }
+
+
+ static void assertPathBlocked(InvalidRequestFilter filter, String path) {
+ assertThat "Expected path '${path}', to be blocked", !filter.isAccessAllowed(mockRequest(path), null, null)
+ }
+
+ static void assertPathAllowed(InvalidRequestFilter filter, String path) {
+ assertThat "Expected path '${path}', to be allowed", filter.isAccessAllowed(mockRequest(path), null, null)
+ }
+
+ static HttpServletRequest mockRequest(String path) {
+ HttpServletRequest request = mock(HttpServletRequest)
+ expect(request.getRequestURI()).andReturn(path)
+ replay(request)
+ return request
+ }
+}
diff --git a/web/src/test/groovy/org/apache/shiro/web/filter/mgt/DefaultFilterChainManagerTest.groovy b/web/src/test/groovy/org/apache/shiro/web/filter/mgt/DefaultFilterChainManagerTest.groovy
index 364300b..8733588 100644
--- a/web/src/test/groovy/org/apache/shiro/web/filter/mgt/DefaultFilterChainManagerTest.groovy
+++ b/web/src/test/groovy/org/apache/shiro/web/filter/mgt/DefaultFilterChainManagerTest.groovy
@@ -21,6 +21,7 @@
import org.apache.shiro.config.ConfigurationException
import org.apache.shiro.web.filter.authz.SslFilter
import org.apache.shiro.web.servlet.ShiroFilter
+import org.hamcrest.Matchers
import javax.servlet.Filter
import javax.servlet.FilterChain
@@ -31,6 +32,7 @@
import static org.easymock.EasyMock.*
import static org.junit.Assert.*
+import static org.hamcrest.MatcherAssert.assertThat
/**
* Unit tests for the {@link DefaultFilterChainManager} implementation.
@@ -210,6 +212,56 @@
assertEquals(DefaultFilter.perms.getFilterClass(), filter.getClass());
}
+ @Test
+ void testWithGlobalFilters() {
+ DefaultFilterChainManager manager = new DefaultFilterChainManager()
+ manager.setGlobalFilters(["invalidRequest", "port"])
+ assertThat(manager.filterChains, Matchers.anEmptyMap())
+
+ // add a chain
+ manager.createChain("test", "authc, roles[manager], perms[\"user:read,write:12345\"")
+
+ assertThat(manager.getChain("test"), Matchers.contains(
+ Matchers.instanceOf(DefaultFilter.invalidRequest.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.port.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.authc.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.roles.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.perms.getFilterClass())
+ ))
+
+ // the "default" chain doesn't exist until it is created
+ assertThat(manager.getChain("/**"), Matchers.nullValue())
+ // create it
+ manager.createDefaultChain("/**")
+ // verify it
+ assertThat(manager.getChain("/**"), Matchers.contains(
+ Matchers.instanceOf(DefaultFilter.invalidRequest.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.port.getFilterClass())
+ ))
+ }
+
+ @Test
+ void addDefaultChainWithSameName() {
+
+ DefaultFilterChainManager manager = new DefaultFilterChainManager()
+ manager.setGlobalFilters(["invalidRequest", "port"])
+
+ // create a chain
+ manager.createChain("test", "authc")
+
+ // create the default chain with the same name
+ manager.createDefaultChain("test")
+
+ // since the "default" chain was created with the same name as an existing chain, we could end up adding the
+ // global filters to the chain twice, test to verify it is only once
+ assertThat(manager.getChain("test"), Matchers.contains(
+ Matchers.instanceOf(DefaultFilter.invalidRequest.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.port.getFilterClass()),
+ Matchers.instanceOf(DefaultFilter.authc.getFilterClass())
+ ))
+
+ }
+
/**
* Helps assert <a href="https://issues.apache.org/jira/browse/SHIRO-429">SHIRO-429</a>
* @since 1.2.2
diff --git a/web/src/test/java/org/apache/shiro/web/env/FilterStub.java b/web/src/test/java/org/apache/shiro/web/env/FilterStub.java
new file mode 100644
index 0000000..1330974
--- /dev/null
+++ b/web/src/test/java/org/apache/shiro/web/env/FilterStub.java
@@ -0,0 +1,45 @@
+/*
+ * 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.env;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+
+public class FilterStub implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+}