Skip to content

Commit 8061318

Browse files
Polish SecurityFilterChain Validation
Closes gh-15982
1 parent b9f3a28 commit 8061318

File tree

5 files changed

+134
-15
lines changed

5 files changed

+134
-15
lines changed

config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java

+23-13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.security.web.FilterChainProxy;
4040
import org.springframework.security.web.FilterInvocation;
4141
import org.springframework.security.web.SecurityFilterChain;
42+
import org.springframework.security.web.UnreachableFilterChainException;
4243
import org.springframework.security.web.access.ExceptionTranslationFilter;
4344
import org.springframework.security.web.access.intercept.AuthorizationFilter;
4445
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
@@ -53,7 +54,6 @@
5354
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
5455
import org.springframework.security.web.session.SessionManagementFilter;
5556
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
56-
import org.springframework.security.web.util.matcher.RequestMatcher;
5757

5858
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
5959

@@ -75,25 +75,35 @@ private void checkPathOrder(List<SecurityFilterChain> filterChains) {
7575
// Check that the universal pattern is listed at the end, if at all
7676
Iterator<SecurityFilterChain> chains = filterChains.iterator();
7777
while (chains.hasNext()) {
78-
RequestMatcher matcher = ((DefaultSecurityFilterChain) chains.next()).getRequestMatcher();
79-
if (AnyRequestMatcher.INSTANCE.equals(matcher) && chains.hasNext()) {
80-
throw new IllegalArgumentException("A universal match pattern ('/**') is defined "
81-
+ " before other patterns in the filter chain, causing them to be ignored. Please check the "
82-
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration");
78+
if (chains.next() instanceof DefaultSecurityFilterChain securityFilterChain) {
79+
if (AnyRequestMatcher.INSTANCE.equals(securityFilterChain.getRequestMatcher()) && chains.hasNext()) {
80+
throw new UnreachableFilterChainException("A universal match pattern ('/**') is defined "
81+
+ " before other patterns in the filter chain, causing them to be ignored. Please check the "
82+
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration",
83+
securityFilterChain, chains.next());
84+
}
8385
}
8486
}
8587
}
8688

8789
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
88-
while (chains.size() > 1) {
89-
DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain) chains.remove(0);
90-
for (SecurityFilterChain test : chains) {
91-
if (chain.getRequestMatcher().equals(((DefaultSecurityFilterChain) test).getRequestMatcher())) {
92-
throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the"
93-
+ " matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace "
94-
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.");
90+
DefaultSecurityFilterChain filterChain = null;
91+
for (SecurityFilterChain chain : chains) {
92+
if (filterChain != null) {
93+
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
94+
if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
95+
throw new UnreachableFilterChainException(
96+
"The FilterChainProxy contains two filter chains using the" + " matcher "
97+
+ defaultChain.getRequestMatcher()
98+
+ ". If you are using multiple <http> namespace "
99+
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.",
100+
defaultChain, chain);
101+
}
95102
}
96103
}
104+
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
105+
filterChain = defaultChain;
106+
}
97107
}
98108
}
99109

config/src/test/java/org/springframework/security/config/http/DefaultFilterChainValidatorTests.java

+23
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.security.config.http;
1818

19+
import java.util.ArrayList;
1920
import java.util.Collection;
21+
import java.util.List;
2022

2123
import jakarta.servlet.http.HttpServletRequest;
2224
import org.apache.commons.logging.Log;
@@ -33,16 +35,20 @@
3335
import org.springframework.security.web.AuthenticationEntryPoint;
3436
import org.springframework.security.web.DefaultSecurityFilterChain;
3537
import org.springframework.security.web.FilterChainProxy;
38+
import org.springframework.security.web.SecurityFilterChain;
39+
import org.springframework.security.web.UnreachableFilterChainException;
3640
import org.springframework.security.web.access.ExceptionTranslationFilter;
3741
import org.springframework.security.web.access.intercept.AuthorizationFilter;
3842
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
3943
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
4044
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
4145
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
4246
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
47+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4348
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
4449
import org.springframework.test.util.ReflectionTestUtils;
4550

51+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4652
import static org.mockito.ArgumentMatchers.any;
4753
import static org.mockito.BDDMockito.given;
4854
import static org.mockito.BDDMockito.willThrow;
@@ -130,4 +136,21 @@ public void validateCustomMetadataSource() {
130136
verify(customMetaDataSource, atLeastOnce()).getAttributes(any());
131137
}
132138

139+
@Test
140+
void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() {
141+
AnonymousAuthenticationFilter authenticationFilter = mock(AnonymousAuthenticationFilter.class);
142+
ExceptionTranslationFilter exceptionTranslationFilter = mock(ExceptionTranslationFilter.class);
143+
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
144+
authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor);
145+
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
146+
authenticationFilter, exceptionTranslationFilter, this.authorizationInterceptor);
147+
List<SecurityFilterChain> chains = new ArrayList<>();
148+
chains.add(chain2);
149+
chains.add(chain1);
150+
FilterChainProxy proxy = new FilterChainProxy(chains);
151+
152+
assertThatExceptionOfType(UnreachableFilterChainException.class)
153+
.isThrownBy(() -> this.validator.validate(proxy));
154+
}
155+
133156
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web;
18+
19+
/**
20+
* Thrown if {@link SecurityFilterChain securityFilterChain} is not valid.
21+
*
22+
* @author Max Batischev
23+
* @since 6.5
24+
*/
25+
public class UnreachableFilterChainException extends IllegalArgumentException {
26+
27+
private final SecurityFilterChain filterChain;
28+
29+
private final SecurityFilterChain unreachableFilterChain;
30+
31+
/**
32+
* Constructs an <code>UnreachableFilterChainException</code> with the specified
33+
* message.
34+
* @param message the detail message
35+
*/
36+
public UnreachableFilterChainException(String message, SecurityFilterChain filterChain,
37+
SecurityFilterChain unreachableFilterChain) {
38+
super(message);
39+
this.filterChain = filterChain;
40+
this.unreachableFilterChain = unreachableFilterChain;
41+
}
42+
43+
public SecurityFilterChain getFilterChain() {
44+
return filterChain;
45+
}
46+
47+
public SecurityFilterChain getUnreachableFilterChain() {
48+
return unreachableFilterChain;
49+
}
50+
}

web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.util.LinkedHashMap;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Objects;
2324

2425
import jakarta.servlet.http.HttpServletRequest;
2526
import org.apache.commons.logging.Log;
@@ -90,6 +91,23 @@ public MatchResult matcher(HttpServletRequest request) {
9091
return MatchResult.match(variables);
9192
}
9293

94+
@Override
95+
public boolean equals(Object o) {
96+
if (this == o) {
97+
return true;
98+
}
99+
if (o == null || getClass() != o.getClass()) {
100+
return false;
101+
}
102+
AndRequestMatcher that = (AndRequestMatcher) o;
103+
return Objects.equals(this.requestMatchers, that.requestMatchers);
104+
}
105+
106+
@Override
107+
public int hashCode() {
108+
return Objects.hash(this.requestMatchers);
109+
}
110+
93111
@Override
94112
public String toString() {
95113
return "And " + this.requestMatchers;

web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.util.Arrays;
2020
import java.util.List;
21+
import java.util.Objects;
2122

2223
import jakarta.servlet.http.HttpServletRequest;
2324

@@ -81,6 +82,23 @@ public MatchResult matcher(HttpServletRequest request) {
8182
return MatchResult.notMatch();
8283
}
8384

85+
@Override
86+
public boolean equals(Object o) {
87+
if (this == o) {
88+
return true;
89+
}
90+
if (o == null || getClass() != o.getClass()) {
91+
return false;
92+
}
93+
OrRequestMatcher that = (OrRequestMatcher) o;
94+
return Objects.equals(this.requestMatchers, that.requestMatchers);
95+
}
96+
97+
@Override
98+
public int hashCode() {
99+
return Objects.hash(this.requestMatchers);
100+
}
101+
84102
@Override
85103
public String toString() {
86104
return "Or " + this.requestMatchers;

0 commit comments

Comments
 (0)