Polish SecurityFilterChain Validation

Issue gh-15982
This commit is contained in:
Max Batischev 2024-12-17 14:00:29 +03:00 committed by Josh Cummings
parent fa58ebbc0c
commit e9bdb5b96e
6 changed files with 139 additions and 15 deletions

View File

@ -39,6 +39,7 @@ import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
@ -53,7 +54,6 @@ import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator { public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
@ -75,26 +75,36 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain
// Check that the universal pattern is listed at the end, if at all // Check that the universal pattern is listed at the end, if at all
Iterator<SecurityFilterChain> chains = filterChains.iterator(); Iterator<SecurityFilterChain> chains = filterChains.iterator();
while (chains.hasNext()) { while (chains.hasNext()) {
RequestMatcher matcher = ((DefaultSecurityFilterChain) chains.next()).getRequestMatcher(); if (chains.next() instanceof DefaultSecurityFilterChain securityFilterChain) {
if (AnyRequestMatcher.INSTANCE.equals(matcher) && chains.hasNext()) { if (AnyRequestMatcher.INSTANCE.equals(securityFilterChain.getRequestMatcher()) && chains.hasNext()) {
throw new IllegalArgumentException("A universal match pattern ('/**') is defined " throw new UnreachableFilterChainException("A universal match pattern ('/**') is defined "
+ " before other patterns in the filter chain, causing them to be ignored. Please check the " + " before other patterns in the filter chain, causing them to be ignored. Please check the "
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration"); + "ordering in your <security:http> namespace or FilterChainProxy bean configuration",
securityFilterChain, chains.next());
}
} }
} }
} }
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) { private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
while (chains.size() > 1) { DefaultSecurityFilterChain filterChain = null;
DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain) chains.remove(0); for (SecurityFilterChain chain : chains) {
for (SecurityFilterChain test : chains) { if (filterChain != null) {
if (chain.getRequestMatcher().equals(((DefaultSecurityFilterChain) test).getRequestMatcher())) { if (chain instanceof DefaultSecurityFilterChain defaultChain) {
throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the" if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
+ " matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace " throw new UnreachableFilterChainException(
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply."); "The FilterChainProxy contains two filter chains using the" + " matcher "
+ defaultChain.getRequestMatcher()
+ ". If you are using multiple <http> namespace "
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.",
defaultChain, chain);
} }
} }
} }
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
filterChain = defaultChain;
}
}
} }
@SuppressWarnings({ "unchecked" }) @SuppressWarnings({ "unchecked" })

View File

@ -16,7 +16,9 @@
package org.springframework.security.config.http; package org.springframework.security.config.http;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -33,6 +35,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
@ -40,9 +44,11 @@ import org.springframework.security.web.access.intercept.FilterInvocationSecurit
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow; import static org.mockito.BDDMockito.willThrow;
@ -130,4 +136,21 @@ public class DefaultFilterChainValidatorTests {
verify(customMetaDataSource, atLeastOnce()).getAttributes(any()); verify(customMetaDataSource, atLeastOnce()).getAttributes(any());
} }
@Test
void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() {
AnonymousAuthenticationFilter authenticationFilter = mock(AnonymousAuthenticationFilter.class);
ExceptionTranslationFilter exceptionTranslationFilter = mock(ExceptionTranslationFilter.class);
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
} }

View File

@ -0,0 +1,55 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.web;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* Thrown if {@link SecurityFilterChain securityFilterChain} is not valid.
*
* @author Max Batischev
* @since 6.5
*/
public class UnreachableFilterChainException extends IllegalArgumentException {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final SecurityFilterChain filterChain;
private final SecurityFilterChain unreachableFilterChain;
/**
* Constructs an <code>UnreachableFilterChainException</code> with the specified
* message.
* @param message the detail message
*/
public UnreachableFilterChainException(String message, SecurityFilterChain filterChain,
SecurityFilterChain unreachableFilterChain) {
super(message);
this.filterChain = filterChain;
this.unreachableFilterChain = unreachableFilterChain;
}
public SecurityFilterChain getFilterChain() {
return this.filterChain;
}
public SecurityFilterChain getUnreachableFilterChain() {
return this.unreachableFilterChain;
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -90,6 +91,23 @@ public final class AndRequestMatcher implements RequestMatcher {
return MatchResult.match(variables); return MatchResult.match(variables);
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AndRequestMatcher that = (AndRequestMatcher) o;
return Objects.equals(this.requestMatchers, that.requestMatchers);
}
@Override
public int hashCode() {
return Objects.hash(this.requestMatchers);
}
@Override @Override
public String toString() { public String toString() {
return "And " + this.requestMatchers; return "And " + this.requestMatchers;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.springframework.security.web.util.matcher;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -81,6 +82,23 @@ public final class OrRequestMatcher implements RequestMatcher {
return MatchResult.notMatch(); return MatchResult.notMatch();
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OrRequestMatcher that = (OrRequestMatcher) o;
return Objects.equals(this.requestMatchers, that.requestMatchers);
}
@Override
public int hashCode() {
return Objects.hash(this.requestMatchers);
}
@Override @Override
public String toString() { public String toString() {
return "Or " + this.requestMatchers; return "Or " + this.requestMatchers;