Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdad14af63 | |||
| 077c9e0b3e | |||
| dc648cf79f | |||
| c2d2914a4f | |||
| a40f73521c | |||
| 70863952ae | |||
| af95be34c6 | |||
| 71478c2534 | |||
| b289d12aad | |||
| e2361c5c79 | |||
| 8b9c8ca96c | |||
| 44e672d969 | |||
| 330f0f050d | |||
| e45dcb3ab2 | |||
| d3a451fffb | |||
| 9d378103b0 | |||
| 2031f101dc | |||
| 2a2c2dd209 |
@@ -95,11 +95,15 @@ jobs:
|
||||
mkdir -p ~/.gradle
|
||||
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
|
||||
- name: Check samples project
|
||||
env:
|
||||
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
|
||||
SAMPLES_INIT_SCRIPT: ${{ github.workspace }}/build/includeRepo/spring-security-ci.gradle
|
||||
run: |
|
||||
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
|
||||
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
|
||||
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
|
||||
./gradlew checkSamples --stacktrace
|
||||
./gradlew publishMavenJavaPublicationToLocalRepository
|
||||
./gradlew checkSamples -PsamplesInitScript="$SAMPLES_INIT_SCRIPT" -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" --stacktrace
|
||||
check_tangles:
|
||||
name: Check for Package Tangles
|
||||
needs: [ prerequisites ]
|
||||
|
||||
@@ -154,6 +154,10 @@ tasks.register('checkSamples') {
|
||||
includeCheckRemote {
|
||||
repository = 'spring-projects/spring-security-samples'
|
||||
ref = samplesBranch
|
||||
if (project.hasProperty("samplesInitScript")) {
|
||||
initScripts = [samplesInitScript]
|
||||
projectProperties = ["localRepositoryPath": localRepositoryPath, "springSecurityVersion": project.version]
|
||||
}
|
||||
}
|
||||
dependsOn checkRemote
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ dependencies {
|
||||
implementation localGroovy()
|
||||
|
||||
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
|
||||
implementation 'io.projectreactor:reactor-core:3.4.17'
|
||||
implementation 'io.projectreactor:reactor-core:3.4.18'
|
||||
implementation 'gradle.plugin.org.gretty:gretty:3.0.1'
|
||||
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
|
||||
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
|
||||
|
||||
+17
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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
|
||||
@@ -19,7 +19,6 @@ package io.spring.gradle.convention
|
||||
import io.spring.gradle.IncludeRepoTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.GradleBuild
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
|
||||
@@ -40,6 +39,12 @@ class IncludeCheckRemotePlugin implements Plugin<Project> {
|
||||
it.dependsOn 'includeRepo'
|
||||
it.dir = includeRepoTask.get().outputDirectory
|
||||
it.tasks = extension.getTasks()
|
||||
extension.getInitScripts().forEach {script ->
|
||||
it.startParameter.addInitScript(new File(script))
|
||||
}
|
||||
extension.getProjectProperties().entrySet().forEach { entry ->
|
||||
it.startParameter.projectProperties.put(entry.getKey(), entry.getValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +65,16 @@ class IncludeCheckRemotePlugin implements Plugin<Project> {
|
||||
*/
|
||||
List<String> tasks = ['check']
|
||||
|
||||
/**
|
||||
* Init scripts for the build
|
||||
*/
|
||||
List<String> initScripts = []
|
||||
|
||||
/**
|
||||
* Map of properties for the build
|
||||
*/
|
||||
Map<String, String> projectProperties = [:]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package io.spring.gradle.convention;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.spring.gradle.IncludeRepoTask;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.gradle.api.Project;
|
||||
@@ -24,8 +29,6 @@ import org.gradle.testfixtures.ProjectBuilder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class IncludeCheckRemotePluginTest {
|
||||
@@ -68,6 +71,40 @@ class IncludeCheckRemotePluginTest {
|
||||
assertThat(checkRemote.getTasks()).containsExactly("clean", "build", "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenExtensionPropertiesInitScriptsThenCreateCheckRemoteWithProvidedTasks() {
|
||||
this.rootProject = ProjectBuilder.builder().build();
|
||||
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
|
||||
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
|
||||
(includeCheckRemoteExtension) -> {
|
||||
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
|
||||
includeCheckRemoteExtension.setProperty("ref", "main");
|
||||
includeCheckRemoteExtension.setProperty("initScripts", Arrays.asList("spring-security-ci.gradle"));
|
||||
});
|
||||
|
||||
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
|
||||
assertThat(checkRemote.getStartParameter().getAllInitScripts()).extracting(File::getName).containsExactly("spring-security-ci.gradle");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenExtensionPropertiesBuildPropertiesThenCreateCheckRemoteWithProvidedTasks() {
|
||||
Map<String, String> projectProperties = new HashMap<>();
|
||||
projectProperties.put("localRepositoryPath", "~/local/repository");
|
||||
projectProperties.put("anotherProperty", "some_value");
|
||||
this.rootProject = ProjectBuilder.builder().build();
|
||||
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
|
||||
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
|
||||
(includeCheckRemoteExtension) -> {
|
||||
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
|
||||
includeCheckRemoteExtension.setProperty("ref", "main");
|
||||
includeCheckRemoteExtension.setProperty("projectProperties", projectProperties);
|
||||
});
|
||||
|
||||
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
|
||||
assertThat(checkRemote.getStartParameter().getProjectProperties()).containsEntry("localRepositoryPath", "~/local/repository")
|
||||
.containsEntry("anotherProperty", "some_value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenExtensionPropertiesThenRegisterIncludeRepoTaskWithExtensionProperties() {
|
||||
this.rootProject = ProjectBuilder.builder().build();
|
||||
|
||||
+14
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -3283,20 +3283,26 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
*/
|
||||
public final class MvcMatchersRequestMatcherConfigurer extends RequestMatcherConfigurer {
|
||||
|
||||
private final List<MvcRequestMatcher> mvcMatchers;
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
* @param context the {@link ApplicationContext} to use
|
||||
* @param matchers the {@link MvcRequestMatcher} instances to set the servlet path
|
||||
* on if {@link #servletPath(String)} is set.
|
||||
* @param mvcMatchers the {@link MvcRequestMatcher} instances to set the servlet
|
||||
* path on if {@link #servletPath(String)} is set.
|
||||
* @param allMatchers the {@link RequestMatcher} instances to continue the
|
||||
* configuration
|
||||
*/
|
||||
private MvcMatchersRequestMatcherConfigurer(ApplicationContext context, List<MvcRequestMatcher> matchers) {
|
||||
private MvcMatchersRequestMatcherConfigurer(ApplicationContext context, List<MvcRequestMatcher> mvcMatchers,
|
||||
List<RequestMatcher> allMatchers) {
|
||||
super(context);
|
||||
this.matchers = new ArrayList<>(matchers);
|
||||
this.mvcMatchers = new ArrayList<>(mvcMatchers);
|
||||
this.matchers = allMatchers;
|
||||
}
|
||||
|
||||
public RequestMatcherConfigurer servletPath(String servletPath) {
|
||||
for (RequestMatcher matcher : this.matchers) {
|
||||
((MvcRequestMatcher) matcher).setServletPath(servletPath);
|
||||
for (MvcRequestMatcher matcher : this.mvcMatchers) {
|
||||
matcher.setServletPath(servletPath);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -3321,7 +3327,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
public MvcMatchersRequestMatcherConfigurer mvcMatchers(HttpMethod method, String... mvcPatterns) {
|
||||
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
|
||||
setMatchers(mvcMatchers);
|
||||
return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers);
|
||||
return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers, this.matchers);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-1
@@ -344,7 +344,10 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
|
||||
if (filter instanceof AuthorizationFilter) {
|
||||
AuthorizationManager<HttpServletRequest> authorizationManager = ((AuthorizationFilter) filter)
|
||||
.getAuthorizationManager();
|
||||
privilegeEvaluators.add(new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager));
|
||||
AuthorizationManagerWebInvocationPrivilegeEvaluator evaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator(
|
||||
authorizationManager);
|
||||
evaluator.setServletContext(this.servletContext);
|
||||
privilegeEvaluators.add(evaluator);
|
||||
}
|
||||
}
|
||||
return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators);
|
||||
|
||||
+80
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -21,17 +21,22 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.web.PortMapperImpl;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
|
||||
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
|
||||
import org.springframework.security.web.access.channel.InsecureChannelProcessor;
|
||||
import org.springframework.security.web.access.channel.SecureChannelProcessor;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.spy;
|
||||
@@ -44,6 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Eleftheria Stein
|
||||
* @author Onur Kagan Ozcan
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class ChannelSecurityConfigurerTests {
|
||||
@@ -93,6 +99,24 @@ public class ChannelSecurityConfigurerTests {
|
||||
this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/"));
|
||||
}
|
||||
|
||||
// gh-10956
|
||||
@Test
|
||||
public void requestWhenRequiresChannelWithMultiMvcMatchersThenRedirectsToHttps() throws Exception {
|
||||
this.spring.register(RequiresChannelMultiMvcMatchersConfig.class).autowire();
|
||||
this.mvc.perform(get("/test-1")).andExpect(redirectedUrl("https://localhost/test-1"));
|
||||
this.mvc.perform(get("/test-2")).andExpect(redirectedUrl("https://localhost/test-2"));
|
||||
this.mvc.perform(get("/test-3")).andExpect(redirectedUrl("https://localhost/test-3"));
|
||||
}
|
||||
|
||||
// gh-10956
|
||||
@Test
|
||||
public void requestWhenRequiresChannelWithMultiMvcMatchersInLambdaThenRedirectsToHttps() throws Exception {
|
||||
this.spring.register(RequiresChannelMultiMvcMatchersInLambdaConfig.class).autowire();
|
||||
this.mvc.perform(get("/test-1")).andExpect(redirectedUrl("https://localhost/test-1"));
|
||||
this.mvc.perform(get("/test-2")).andExpect(redirectedUrl("https://localhost/test-2"));
|
||||
this.mvc.perform(get("/test-3")).andExpect(redirectedUrl("https://localhost/test-3"));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@@ -155,4 +179,59 @@ public class ChannelSecurityConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class RequiresChannelMultiMvcMatchersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.portMapper()
|
||||
.portMapper(new PortMapperImpl())
|
||||
.and()
|
||||
.requiresChannel()
|
||||
.mvcMatchers("/test-1")
|
||||
.requiresSecure()
|
||||
.mvcMatchers("/test-2")
|
||||
.requiresSecure()
|
||||
.mvcMatchers("/test-3")
|
||||
.requiresSecure()
|
||||
.anyRequest()
|
||||
.requiresInsecure();
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class RequiresChannelMultiMvcMatchersInLambdaConfig {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.portMapper((port) -> port
|
||||
.portMapper(new PortMapperImpl())
|
||||
)
|
||||
.requiresChannel((channel) -> channel
|
||||
.mvcMatchers("/test-1")
|
||||
.requiresSecure()
|
||||
.mvcMatchers("/test-2")
|
||||
.requiresSecure()
|
||||
.mvcMatchers("/test-3")
|
||||
.requiresSecure()
|
||||
.anyRequest()
|
||||
.requiresInsecure()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+132
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -23,7 +23,10 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
@@ -33,6 +36,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
@@ -167,6 +171,38 @@ public class HttpSecurityRequestMatchersTests {
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatcherWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception {
|
||||
loadConfig(MultiMvcMatcherInLambdaConfig.class);
|
||||
this.request.setRequestURI("/test-1");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
setup();
|
||||
this.request.setRequestURI("/test-2");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
setup();
|
||||
this.request.setRequestURI("/test-3");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMatcherWhenMultiMvcMatcherThenAllPathsAreDenied() throws Exception {
|
||||
loadConfig(MultiMvcMatcherConfig.class);
|
||||
this.request.setRequestURI("/test-1");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
setup();
|
||||
this.request.setRequestURI("/test-2");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
setup();
|
||||
this.request.setRequestURI("/test-3");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
@@ -175,6 +211,101 @@ public class HttpSecurityRequestMatchersTests {
|
||||
this.context.getAutowireCapableBeanFactory().autowireBean(this);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
static class MultiMvcMatcherInLambdaConfig {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
SecurityFilterChain first(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatchers((requests) -> requests
|
||||
.mvcMatchers("/test-1")
|
||||
.mvcMatchers("/test-2")
|
||||
.mvcMatchers("/test-3")
|
||||
)
|
||||
.authorizeRequests((authorize) -> authorize.anyRequest().denyAll())
|
||||
.httpBasic(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain second(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatchers((requests) -> requests
|
||||
.mvcMatchers("/test-1")
|
||||
)
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping({ "/test-1", "/test-2", "/test-3" })
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
static class MultiMvcMatcherConfig {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
SecurityFilterChain first(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatchers()
|
||||
.mvcMatchers("/test-1")
|
||||
.mvcMatchers("/test-2")
|
||||
.mvcMatchers("/test-3")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.anyRequest().denyAll()
|
||||
.and()
|
||||
.httpBasic(withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain second(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatchers()
|
||||
.mvcMatchers("/test-1")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.anyRequest().permitAll();
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping({ "/test-1", "/test-2", "/test-3" })
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
|
||||
+77
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -23,16 +25,24 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
@@ -125,6 +135,35 @@ public class UrlAuthorizationConfigurerTests {
|
||||
loadConfig(AnonymousUrlAuthorizationConfig.class);
|
||||
}
|
||||
|
||||
// gh-10956
|
||||
@Test
|
||||
public void multiMvcMatchersConfig() throws Exception {
|
||||
loadConfig(MultiMvcMatcherConfig.class);
|
||||
this.request.addHeader("Authorization",
|
||||
"Basic " + new String(Base64.getEncoder().encode("user:password".getBytes())));
|
||||
this.request.setRequestURI("/test-1");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
setup();
|
||||
this.request.addHeader("Authorization",
|
||||
"Basic " + new String(Base64.getEncoder().encode("user:password".getBytes())));
|
||||
this.request.setRequestURI("/test-2");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
setup();
|
||||
this.request.addHeader("Authorization",
|
||||
"Basic " + new String(Base64.getEncoder().encode("user:password".getBytes())));
|
||||
this.request.setRequestURI("/test-3");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
setup();
|
||||
this.request.addHeader("Authorization",
|
||||
"Basic " + new String(Base64.getEncoder().encode("user:password".getBytes())));
|
||||
this.request.setRequestURI("/test-x");
|
||||
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
@@ -228,4 +267,41 @@ public class UrlAuthorizationConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class MultiMvcMatcherConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain security(HttpSecurity http, ApplicationContext context) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.apply(new UrlAuthorizationConfigurer<>(context)).getRegistry()
|
||||
.mvcMatchers("/test-1").hasRole("ADMIN")
|
||||
.mvcMatchers("/test-2").hasRole("ADMIN")
|
||||
.mvcMatchers("/test-3").hasRole("ADMIN")
|
||||
.anyRequest().hasRole("USER");
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class PathController {
|
||||
|
||||
@RequestMapping({ "/test-1", "/test-2", "/test-3", "/test-x" })
|
||||
String path() {
|
||||
return "path";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -526,35 +526,47 @@ public class BCrypt {
|
||||
* @param safety bit 16 is set when the safety measure is requested
|
||||
* @return an array containing the binary hashed password
|
||||
*/
|
||||
private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, boolean sign_ext_bug, int safety) {
|
||||
int rounds, i, j;
|
||||
private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, boolean sign_ext_bug, int safety,
|
||||
boolean for_check) {
|
||||
int cdata[] = bf_crypt_ciphertext.clone();
|
||||
int clen = cdata.length;
|
||||
byte ret[];
|
||||
|
||||
long rounds;
|
||||
if (log_rounds < 4 || log_rounds > 31) {
|
||||
throw new IllegalArgumentException("Bad number of rounds");
|
||||
if (!for_check) {
|
||||
throw new IllegalArgumentException("Bad number of rounds");
|
||||
}
|
||||
if (log_rounds != 0) {
|
||||
throw new IllegalArgumentException("Bad number of rounds");
|
||||
}
|
||||
rounds = 0;
|
||||
}
|
||||
rounds = 1 << log_rounds;
|
||||
else {
|
||||
rounds = roundsForLogRounds(log_rounds);
|
||||
if (rounds < 16 || rounds > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Bad number of rounds");
|
||||
}
|
||||
}
|
||||
|
||||
if (salt.length != BCRYPT_SALT_LEN) {
|
||||
throw new IllegalArgumentException("Bad salt length");
|
||||
}
|
||||
|
||||
init_key();
|
||||
ekskey(salt, password, sign_ext_bug, safety);
|
||||
for (i = 0; i < rounds; i++) {
|
||||
for (int i = 0; i < rounds; i++) {
|
||||
key(password, sign_ext_bug, safety);
|
||||
key(salt, false, safety);
|
||||
}
|
||||
|
||||
for (i = 0; i < 64; i++) {
|
||||
for (j = 0; j < (clen >> 1); j++) {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
for (int j = 0; j < (clen >> 1); j++) {
|
||||
encipher(cdata, j << 1);
|
||||
}
|
||||
}
|
||||
|
||||
ret = new byte[clen * 4];
|
||||
for (i = 0, j = 0; i < clen; i++) {
|
||||
byte[] ret = new byte[clen * 4];
|
||||
for (int i = 0, j = 0; i < clen; i++) {
|
||||
ret[j++] = (byte) ((cdata[i] >> 24) & 0xff);
|
||||
ret[j++] = (byte) ((cdata[i] >> 16) & 0xff);
|
||||
ret[j++] = (byte) ((cdata[i] >> 8) & 0xff);
|
||||
@@ -563,6 +575,10 @@ public class BCrypt {
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static String hashpwforcheck(byte[] passwordb, String salt) {
|
||||
return hashpw(passwordb, salt, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a password using the OpenBSD bcrypt scheme
|
||||
* @param password the password to hash
|
||||
@@ -584,6 +600,10 @@ public class BCrypt {
|
||||
* @return the hashed password
|
||||
*/
|
||||
public static String hashpw(byte passwordb[], String salt) {
|
||||
return hashpw(passwordb, salt, false);
|
||||
}
|
||||
|
||||
private static String hashpw(byte passwordb[], String salt, boolean for_check) {
|
||||
BCrypt B;
|
||||
String real_salt;
|
||||
byte saltb[], hashed[];
|
||||
@@ -633,7 +653,7 @@ public class BCrypt {
|
||||
}
|
||||
|
||||
B = new BCrypt();
|
||||
hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);
|
||||
hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0, for_check);
|
||||
|
||||
rs.append("$2");
|
||||
if (minor >= 'a') {
|
||||
@@ -740,7 +760,8 @@ public class BCrypt {
|
||||
* @return true if the passwords match, false otherwise
|
||||
*/
|
||||
public static boolean checkpw(String plaintext, String hashed) {
|
||||
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
|
||||
byte[] passwordb = plaintext.getBytes(StandardCharsets.UTF_8);
|
||||
return equalsNoEarlyReturn(hashed, hashpwforcheck(passwordb, hashed));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -751,7 +772,7 @@ public class BCrypt {
|
||||
* @since 5.3
|
||||
*/
|
||||
public static boolean checkpw(byte[] passwordb, String hashed) {
|
||||
return equalsNoEarlyReturn(hashed, hashpw(passwordb, hashed));
|
||||
return equalsNoEarlyReturn(hashed, hashpwforcheck(passwordb, hashed));
|
||||
}
|
||||
|
||||
static boolean equalsNoEarlyReturn(String a, String b) {
|
||||
|
||||
+14
@@ -208,4 +208,18 @@ public class BCryptPasswordEncoderTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> encoder.matches(null, "does-not-matter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upgradeWhenNoRoundsThenTrue() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
assertThat(encoder.upgradeEncoding("$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkWhenNoRoundsThenTrue() {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
assertThat(encoder.matches("password", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue"))
|
||||
.isTrue();
|
||||
assertThat(encoder.matches("wrong", "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue")).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -456,4 +456,11 @@ public class BCryptTests {
|
||||
assertThat(BCrypt.equalsNoEarlyReturn("test", "pass")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpwWhenZeroRoundsThenMatches() {
|
||||
String password = "$2a$00$9N8N35BVs5TLqGL3pspAte5OWWA2a2aZIs.EGp7At7txYakFERMue";
|
||||
assertThat(BCrypt.checkpw("password", password)).isTrue();
|
||||
assertThat(BCrypt.checkpw("wrong", password)).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -8,10 +8,10 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.18")
|
||||
api platform("io.projectreactor:reactor-bom:2020.0.19")
|
||||
api platform("io.rsocket:rsocket-bom:1.1.2")
|
||||
api platform("org.junit:junit-bom:5.8.2")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.1.3")
|
||||
api platform("org.springframework.data:spring-data-bom:2021.1.4")
|
||||
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
|
||||
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328")
|
||||
@@ -25,7 +25,7 @@ dependencies {
|
||||
api "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
api "commons-codec:commons-codec:1.15"
|
||||
api "commons-collections:commons-collections:3.2.2"
|
||||
api "io.mockk:mockk:1.12.3"
|
||||
api "io.mockk:mockk:1.12.4"
|
||||
api "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
|
||||
api "jakarta.inject:jakarta.inject-api:1.0.5"
|
||||
api "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
name: ROOT
|
||||
version: '5.6.3'
|
||||
name: 'ROOT'
|
||||
version: '5.6.5'
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
aspectjVersion=1.9.9.1
|
||||
springJavaformatVersion=0.0.31
|
||||
springBootVersion=2.4.2
|
||||
springFrameworkVersion=5.3.19
|
||||
springFrameworkVersion=5.3.20
|
||||
openSamlVersion=3.4.6
|
||||
version=5.6.3
|
||||
version=5.6.5
|
||||
kotlinVersion=1.5.32
|
||||
samplesBranch=5.6.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
|
||||
@@ -14,3 +14,6 @@ changelog:
|
||||
emoji: ":hammer:"
|
||||
labels: ["type: dependency-upgrade"]
|
||||
sort: "title"
|
||||
issues:
|
||||
exclude:
|
||||
labels: ["status: duplicate"]
|
||||
|
||||
+87
-5
@@ -107,6 +107,15 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
|
||||
private static final List<String> FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00"));
|
||||
|
||||
private static final List<String> FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A"));
|
||||
|
||||
private static final List<String> FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\r", "%0d", "%0D"));
|
||||
|
||||
private static final List<String> FORBIDDEN_LINE_SEPARATOR = Collections.unmodifiableList(Arrays.asList("\u2028"));
|
||||
|
||||
private static final List<String> FORBIDDEN_PARAGRAPH_SEPARATOR = Collections
|
||||
.unmodifiableList(Arrays.asList("\u2029"));
|
||||
|
||||
private Set<String> encodedUrlBlocklist = new HashSet<>();
|
||||
|
||||
private Set<String> decodedUrlBlocklist = new HashSet<>();
|
||||
@@ -135,10 +144,14 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
|
||||
urlBlocklistsAddAll(FORBIDDEN_BACKSLASH);
|
||||
urlBlocklistsAddAll(FORBIDDEN_NULL);
|
||||
urlBlocklistsAddAll(FORBIDDEN_LF);
|
||||
urlBlocklistsAddAll(FORBIDDEN_CR);
|
||||
|
||||
this.encodedUrlBlocklist.add(ENCODED_PERCENT);
|
||||
this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
|
||||
this.decodedUrlBlocklist.add(PERCENT);
|
||||
this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR);
|
||||
this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,6 +358,69 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL encoded Carriage Return is allowed in the path or not. The
|
||||
* default is not to allow this behavior because it is a frequent source of security
|
||||
* exploits.
|
||||
* @param allowUrlEncodedCarriageReturn if URL encoded Carriage Return is allowed in
|
||||
* the URL or not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedCarriageReturn(boolean allowUrlEncodedCarriageReturn) {
|
||||
if (allowUrlEncodedCarriageReturn) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_CR);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_CR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL encoded Line Feed is allowed in the path or not. The default is
|
||||
* not to allow this behavior because it is a frequent source of security exploits.
|
||||
* @param allowUrlEncodedLineFeed if URL encoded Line Feed is allowed in the URL or
|
||||
* not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedLineFeed(boolean allowUrlEncodedLineFeed) {
|
||||
if (allowUrlEncodedLineFeed) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_LF);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_LF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL encoded paragraph separator is allowed in the path or not. The
|
||||
* default is not to allow this behavior because it is a frequent source of security
|
||||
* exploits.
|
||||
* @param allowUrlEncodedParagraphSeparator if URL encoded paragraph separator is
|
||||
* allowed in the URL or not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedParagraphSeparator(boolean allowUrlEncodedParagraphSeparator) {
|
||||
if (allowUrlEncodedParagraphSeparator) {
|
||||
this.decodedUrlBlocklist.removeAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
|
||||
}
|
||||
else {
|
||||
this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL encoded line separator is allowed in the path or not. The
|
||||
* default is not to allow this behavior because it is a frequent source of security
|
||||
* exploits.
|
||||
* @param allowUrlEncodedLineSeparator if URL encoded line separator is allowed in the
|
||||
* URL or not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedLineSeparator(boolean allowUrlEncodedLineSeparator) {
|
||||
if (allowUrlEncodedLineSeparator) {
|
||||
this.decodedUrlBlocklist.removeAll(FORBIDDEN_LINE_SEPARATOR);
|
||||
}
|
||||
else {
|
||||
this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines which header names should be allowed. The default is to reject header
|
||||
@@ -431,14 +507,17 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
if (!isNormalized(request)) {
|
||||
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
|
||||
}
|
||||
String requestUri = request.getRequestURI();
|
||||
if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
|
||||
throw new RequestRejectedException(
|
||||
"The requestURI was rejected because it can only contain printable ASCII characters.");
|
||||
}
|
||||
rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI");
|
||||
return new StrictFirewalledRequest(request);
|
||||
}
|
||||
|
||||
private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) {
|
||||
if (!containsOnlyPrintableAsciiCharacters(toCheck)) {
|
||||
throw new RequestRejectedException(String.format(
|
||||
"The %s was rejected because it can only contain printable ASCII characters.", propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
|
||||
if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
|
||||
return;
|
||||
@@ -526,6 +605,9 @@ public class StrictHttpFirewall implements HttpFirewall {
|
||||
}
|
||||
|
||||
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
|
||||
if (uri == null) {
|
||||
return true;
|
||||
}
|
||||
int length = uri.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char ch = uri.charAt(i);
|
||||
|
||||
+4
-2
@@ -43,7 +43,9 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public final class RegexRequestMatcher implements RequestMatcher {
|
||||
|
||||
private static final int DEFAULT = 0;
|
||||
private static final int DEFAULT = Pattern.DOTALL;
|
||||
|
||||
private static final int CASE_INSENSITIVE = DEFAULT | Pattern.CASE_INSENSITIVE;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RegexRequestMatcher.class);
|
||||
|
||||
@@ -68,7 +70,7 @@ public final class RegexRequestMatcher implements RequestMatcher {
|
||||
* {@link Pattern#CASE_INSENSITIVE} flag set.
|
||||
*/
|
||||
public RegexRequestMatcher(String pattern, String httpMethod, boolean caseInsensitive) {
|
||||
this.pattern = Pattern.compile(pattern, caseInsensitive ? Pattern.CASE_INSENSITIVE : DEFAULT);
|
||||
this.pattern = Pattern.compile(pattern, caseInsensitive ? CASE_INSENSITIVE : DEFAULT);
|
||||
this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null;
|
||||
}
|
||||
|
||||
|
||||
+152
@@ -343,6 +343,12 @@ public class StrictHttpFirewallTests {
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenJapaneseCharacterThenNoException() {
|
||||
this.request.setServletPath("/\u3042");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenExceedsUpperboundAsciiThenException() {
|
||||
this.request.setRequestURI("/\u007f");
|
||||
@@ -364,6 +370,152 @@ public class StrictHttpFirewallTests {
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedThenException() {
|
||||
this.request.setRequestURI("/something%0a/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedThenException() {
|
||||
this.request.setRequestURI("/something%0A/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsLineFeedThenException() {
|
||||
this.request.setRequestURI("/something\n/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsLineFeedThenException() {
|
||||
this.request.setServletPath("/something\n/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnThenException() {
|
||||
this.request.setRequestURI("/something%0d/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnThenException() {
|
||||
this.request.setRequestURI("/something%0D/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsCarriageReturnThenException() {
|
||||
this.request.setRequestURI("/something\r/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsCarriageReturnThenException() {
|
||||
this.request.setServletPath("/something\r/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsLineSeparatorThenException() {
|
||||
this.request.setServletPath("/something\u2028/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorThenException() {
|
||||
this.request.setServletPath("/something\u2029/");
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedLineFeed(true);
|
||||
this.request.setRequestURI("/something%0a/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedLineFeed(true);
|
||||
this.request.setRequestURI("/something%0A/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsLineFeedAndAllowedThenException() {
|
||||
this.firewall.setAllowUrlEncodedLineFeed(true);
|
||||
this.request.setRequestURI("/something\n/");
|
||||
// Expected an error because the line feed is decoded in an encoded part of the
|
||||
// URL
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsLineFeedAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedLineFeed(true);
|
||||
this.request.setServletPath("/something\n/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedCarriageReturn(true);
|
||||
this.request.setRequestURI("/something%0d/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedCarriageReturn(true);
|
||||
this.request.setRequestURI("/something%0D/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenContainsCarriageReturnAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedCarriageReturn(true);
|
||||
this.request.setRequestURI("/something\r/");
|
||||
// Expected an error because the carriage return is decoded in an encoded part of
|
||||
// the URL
|
||||
assertThatExceptionOfType(RequestRejectedException.class)
|
||||
.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsCarriageReturnAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedCarriageReturn(true);
|
||||
this.request.setServletPath("/something\r/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsLineSeparatorAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedLineSeparator(true);
|
||||
this.request.setServletPath("/something\u2028/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedParagraphSeparator(true);
|
||||
this.request.setServletPath("/something\u2029/");
|
||||
this.firewall.getFirewalledRequest(this.request);
|
||||
}
|
||||
|
||||
/**
|
||||
* On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on /a/b/c
|
||||
* because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c while Spring MVC
|
||||
|
||||
+16
@@ -101,6 +101,22 @@ public class RegexRequestMatcherTests {
|
||||
assertThat(matcher.matches(request)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWithCarriageReturn() {
|
||||
RegexRequestMatcher matcher = new RegexRequestMatcher(".*", null);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/blah%0a");
|
||||
request.setServletPath("/blah\n");
|
||||
assertThat(matcher.matches(request)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWithLineFeed() {
|
||||
RegexRequestMatcher matcher = new RegexRequestMatcher(".*", null);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/blah%0d");
|
||||
request.setServletPath("/blah\r");
|
||||
assertThat(matcher.matches(request)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toStringThenFormatted() {
|
||||
RegexRequestMatcher matcher = new RegexRequestMatcher("/blah", "GET");
|
||||
|
||||
Reference in New Issue
Block a user