Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 506e5b7f11 | |||
| a7bf8f7cc6 | |||
| 451fbf0227 | |||
| d985d044bc | |||
| c24b5ebe98 | |||
| fd900c288e | |||
| f28c26fd54 | |||
| 421430330a | |||
| 5c2106b22e | |||
| 48241deba3 | |||
| 9cc11be9f3 | |||
| 1399a82ea9 | |||
| ec33e40748 | |||
| e48d6b039b | |||
| dd3c6892e9 | |||
| f9f533499b | |||
| c552366a78 | |||
| 72a208e02c | |||
| 7215c72373 | |||
| 542071b1f8 | |||
| 4ce7cde155 | |||
| c66ee4a21a | |||
| 517ea7f42a | |||
| fe79766aa2 | |||
| 755e9f2f69 | |||
| 7b7a3044cf | |||
| 8a791028b1 | |||
| 0a4eb0f09a | |||
| a0e6c17512 | |||
| e251779b31 | |||
| 7ad6f38d5b | |||
| 97cefa6830 | |||
| f836efb912 | |||
| e92a945a2d | |||
| 92809cef01 | |||
| 097c927d8d | |||
| 0115e8aea1 | |||
| 6b69edb9a7 | |||
| 0ba26176db | |||
| 85775fe794 | |||
| 1ca53164ee | |||
| 47a521ca26 | |||
| 751c6a762a | |||
| 7d7b59bea4 | |||
| 7149622efd | |||
| e9e0ec89fb | |||
| 596c5f9b0c | |||
| 5d335ccf8d | |||
| 5cdcdc9bcb | |||
| a5b1dec99a | |||
| ea2ec04633 | |||
| c6bc1ec969 | |||
| 9ac5ee9bdb | |||
| 31babecfbd | |||
| 4c12940304 | |||
| bb3738dbfd | |||
| 5c1a108b8b | |||
| 1c4a63387b | |||
| dab48d25b0 | |||
| caa325f92e | |||
| 174e3ef356 | |||
| 32e2735f45 | |||
| 9cfc9bb8a8 | |||
| 341f58d411 | |||
| c00197bdf5 | |||
| 01e1813020 | |||
| 65c037853e | |||
| 05256da801 | |||
| 2bdb57a8a8 | |||
| a3095527a0 | |||
| 9189916287 | |||
| 97e6ac76af | |||
| 779030b6cd | |||
| 1135ad5a5a | |||
| e3d642ce7c | |||
| 00247350d0 | |||
| 9e2cadb475 | |||
| 20332fedb0 | |||
| 2537a033f5 | |||
| f622d8e2e2 | |||
| 92cab2b678 | |||
| 254709ce0b | |||
| e9c1b7b0ae | |||
| 2b322da9ac | |||
| 6aabd768a8 | |||
| cdd626644e | |||
| 5a798e93f1 | |||
| aa88404d69 | |||
| dcb8c563e8 | |||
| e34621ec2c | |||
| 1695d03b72 | |||
| e7ea4091a0 | |||
| 235dfc4dfd | |||
| 092a63d1e0 | |||
| 896dd93313 | |||
| c7b739eb3f | |||
| 32e860523c | |||
| 58232b1172 | |||
| 0e8fd1cd6c | |||
| 1f001f8a6d | |||
| 3fdfa98172 | |||
| 90f5141880 | |||
| ab6667eb5c | |||
| 4b10c2daac | |||
| 63e24a3026 | |||
| 9027dceb71 |
@@ -9,7 +9,6 @@ on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
COMMIT_OWNER: ${{ github.event.pusher.name }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
@@ -209,26 +208,11 @@ jobs:
|
||||
run: |
|
||||
echo "Tagging and publishing $REPO@$VERSION release on GitHub."
|
||||
./gradlew createGitHubRelease -PnextVersion=$VERSION -Pbranch=$BRANCH -PcreateRelease=true -PgitHubAccessToken=$TOKEN
|
||||
- name: Announce Release on Slack
|
||||
id: spring-security-announcing
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "spring-security-announcing `${{ env.VERSION }}` is available now",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "spring-security-announcing `${{ env.VERSION }}` is available now"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
- name: Announce Release on GChat
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }}
|
||||
run: |
|
||||
curl -X POST '${{ env.WEBHOOK_URL }}' -H 'Content-Type: application/json' -d '{ "text": "spring-security-announcing `${{ env.VERSION }}` is available now" }' || true
|
||||
- name: Setup git config
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
@@ -260,20 +244,13 @@ jobs:
|
||||
distribution: 'adopt'
|
||||
- name: Schedule next release (if not already scheduled)
|
||||
run: ./gradlew scheduleNextRelease -PnextVersion=$VERSION -PgitHubAccessToken=$TOKEN
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [perform_release, perform_post_release]
|
||||
if: failure()
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ perform_release, perform_post_release ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
# Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed
|
||||
# See https://github.com/Gamesight/slack-workflow-status/issues/38
|
||||
uses: sjohnr/slack-workflow-status@v1-beta
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Mark Duplicate Dependabot PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
check_duplicate_prs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Dependency Name from PR Title
|
||||
id: extract
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
|
||||
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find PRs
|
||||
id: find_duplicates
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "${{ steps.extract.outputs.dependency_name }}"' --json number --jq 'map(.number) | join(",")')
|
||||
echo "prs=$PRS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Label Duplicate PRs
|
||||
if: steps.find_duplicates.outputs.prs != ''
|
||||
env:
|
||||
PRS: ${{ steps.find_duplicates.outputs.prs }}
|
||||
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
for i in ${PRS//,/ }
|
||||
do
|
||||
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
|
||||
echo "Marking PR $i as duplicate"
|
||||
gh pr edit "$i" --add-label "status: duplicate"
|
||||
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
|
||||
fi
|
||||
done
|
||||
@@ -8,8 +8,9 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
merge-dependabot-pr:
|
||||
name: Merge Dependabot PR
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
@@ -50,3 +51,13 @@ jobs:
|
||||
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ merge-dependabot-pr ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -23,18 +23,13 @@ jobs:
|
||||
export TOOL_VERSION=0.1.1
|
||||
wget "https://repo.maven.apache.org/maven2/io/spring/releasetrain/spring-release-train-tools/$TOOL_VERSION/spring-release-train-tools-$TOOL_VERSION.jar"
|
||||
java -cp "spring-release-train-tools-$TOOL_VERSION.jar" io.spring.releasetrain.CheckMilestoneDueOnMain --dueOn "$DUE_ON" --expectedDayOfWeek MONDAY --expectedMondayCount 3
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [spring-releasetrain-checks]
|
||||
if: failure()
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ spring-releasetrain-checks ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue --scan
|
||||
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue
|
||||
generate-docs:
|
||||
name: Generate Docs
|
||||
runs-on: ubuntu-latest
|
||||
@@ -42,3 +42,13 @@ jobs:
|
||||
name: docs
|
||||
path: docs/build/site
|
||||
overwrite: true
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ build, generate-docs ]
|
||||
if: ${{ failure() && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main.
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
@@ -58,12 +57,13 @@ jobs:
|
||||
git tag $updatedVersion
|
||||
git push
|
||||
git push origin $updatedVersion
|
||||
- id: send-slack-notification
|
||||
name: Send Slack message
|
||||
if: failure()
|
||||
uses: Gamesight/slack-workflow-status@v1.3.0
|
||||
send-notification:
|
||||
name: Send Notification
|
||||
needs: [ update_scheduled_release_version ]
|
||||
if: ${{ failure() || cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Notification
|
||||
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
|
||||
|
||||
@@ -25,6 +25,7 @@ apply plugin: 'org.springframework.github.milestone'
|
||||
apply plugin: 'org.springframework.github.changelog'
|
||||
apply plugin: 'org.springframework.github.release'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
apply plugin: 'org.springframework.security.check-expected-branch-version'
|
||||
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Security'
|
||||
|
||||
@@ -68,6 +68,10 @@ gradlePlugin {
|
||||
id = "org.springframework.security.versions.verify-dependencies-versions"
|
||||
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
|
||||
}
|
||||
checkExpectedBranchVersion {
|
||||
id = "org.springframework.security.check-expected-branch-version"
|
||||
implementationClass = "org.springframework.security.CheckExpectedBranchVersionPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
|
||||
/**
|
||||
* @author Marcus da Coregio
|
||||
*/
|
||||
public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
TaskProvider<CheckExpectedBranchVersionTask> checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> {
|
||||
task.setGroup("Build");
|
||||
task.setDescription("Check if the project version matches the branch version");
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask));
|
||||
}
|
||||
|
||||
public static class CheckExpectedBranchVersionTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Project project = getProject();
|
||||
if (project.hasProperty("skipCheckExpectedBranchVersion")) {
|
||||
return;
|
||||
}
|
||||
String version = (String) project.getVersion();
|
||||
String branchVersion = getBranchVersion(project);
|
||||
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
|
||||
System.out.println("Branch version '" + branchVersion + "' does not match *.x, ignoring");
|
||||
return;
|
||||
}
|
||||
if (!versionsMatch(version, branchVersion)) {
|
||||
throw new IllegalStateException(String.format("Project version [%s] does not match branch version [%s]. " +
|
||||
"Please verify that the branch contains the right version.", version, branchVersion));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getBranchVersion(Project project) throws IOException {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
project.exec((exec) -> {
|
||||
exec.commandLine("git", "symbolic-ref", "--short", "HEAD");
|
||||
exec.setErrorOutput(System.err);
|
||||
exec.setStandardOutput(baos);
|
||||
});
|
||||
return baos.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean versionsMatch(String projectVersion, String branchVersion) {
|
||||
String[] projectVersionParts = projectVersion.split("\\.");
|
||||
String[] branchVersionParts = branchVersion.split("\\.");
|
||||
if (projectVersionParts.length < 2 || branchVersionParts.length < 2) {
|
||||
return false;
|
||||
}
|
||||
return projectVersionParts[0].equals(branchVersionParts[0]) && projectVersionParts[1].equals(branchVersionParts[1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -18,6 +18,7 @@ package org.springframework.security.cas.userdetails;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.jasig.cas.client.validation.Assertion;
|
||||
|
||||
@@ -73,7 +74,8 @@ public final class GrantedAuthorityFromAssertionAttributesUserDetailsService
|
||||
}
|
||||
|
||||
private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) {
|
||||
return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString());
|
||||
return new SimpleGrantedAuthority(
|
||||
this.convertToUpperCase ? o.toString().toUpperCase(Locale.ROOT) : o.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+69
-69
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -43,11 +44,13 @@ import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
|
||||
/**
|
||||
@@ -322,43 +325,22 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
if (servletContext == null) {
|
||||
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
|
||||
}
|
||||
boolean isProgrammaticApiAvailable = isProgrammaticApiAvailable(servletContext);
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
for (String pattern : patterns) {
|
||||
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
|
||||
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
|
||||
if (isProgrammaticApiAvailable) {
|
||||
matchers.add(resolve(ant, mvc, servletContext));
|
||||
}
|
||||
else {
|
||||
this.logger
|
||||
.warn("The ServletRegistration API was not available at startup time. This may be due to a misconfiguration; "
|
||||
+ "if you are using AbstractSecurityWebApplicationInitializer, please double-check the recommendations outlined in "
|
||||
+ "https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#abstractsecuritywebapplicationinitializer-with-spring-mvc");
|
||||
matchers.add(new DeferredRequestMatcher((request) -> resolve(ant, mvc, request.getServletContext()),
|
||||
mvc, ant));
|
||||
}
|
||||
matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant));
|
||||
}
|
||||
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
|
||||
}
|
||||
|
||||
private static boolean isProgrammaticApiAvailable(ServletContext servletContext) {
|
||||
try {
|
||||
servletContext.getServletRegistrations();
|
||||
return true;
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) {
|
||||
Map<String, ? extends ServletRegistration> registrations = mappableServletRegistrations(servletContext);
|
||||
if (registrations.isEmpty()) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
if (!hasDispatcherServlet(registrations)) {
|
||||
return ant;
|
||||
return new DispatcherServletDelegatingRequestMatcher(ant, mvc, new MockMvcRequestMatcher());
|
||||
}
|
||||
ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations);
|
||||
if (dispatcherServlet != null) {
|
||||
@@ -452,7 +434,7 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
}
|
||||
}
|
||||
|
||||
private String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
private static String computeErrorMessage(Collection<? extends ServletRegistration> registrations) {
|
||||
String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. "
|
||||
+ "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); "
|
||||
+ "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n"
|
||||
@@ -593,34 +575,29 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
static class DeferredRequestMatcher implements RequestMatcher {
|
||||
|
||||
final Function<HttpServletRequest, RequestMatcher> requestMatcherFactory;
|
||||
final Function<ServletContext, RequestMatcher> requestMatcherFactory;
|
||||
|
||||
final AtomicReference<String> description = new AtomicReference<>();
|
||||
|
||||
volatile RequestMatcher requestMatcher;
|
||||
final Map<ServletContext, RequestMatcher> requestMatchers = new ConcurrentHashMap<>();
|
||||
|
||||
DeferredRequestMatcher(Function<HttpServletRequest, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (request) -> {
|
||||
if (this.requestMatcher == null) {
|
||||
synchronized (this) {
|
||||
if (this.requestMatcher == null) {
|
||||
this.requestMatcher = resolver.apply(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.requestMatcher;
|
||||
};
|
||||
DeferredRequestMatcher(Function<ServletContext, RequestMatcher> resolver, RequestMatcher... candidates) {
|
||||
this.requestMatcherFactory = (sc) -> this.requestMatchers.computeIfAbsent(sc, resolver);
|
||||
this.description.set("Deferred " + Arrays.toString(candidates));
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(ServletContext servletContext) {
|
||||
return this.requestMatcherFactory.apply(servletContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matches(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
return this.requestMatcherFactory.apply(request).matcher(request);
|
||||
return this.requestMatcherFactory.apply(request.getServletContext()).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -630,53 +607,76 @@ public abstract class AbstractRequestMatcherRegistry<C> {
|
||||
|
||||
}
|
||||
|
||||
static class MockMvcRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return request.getAttribute("org.springframework.test.web.servlet.MockMvc.MVC_RESULT_ATTRIBUTE") != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
DispatcherServletRequestMatcher(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration,
|
||||
() -> computeErrorMessage(this.servletContext.getServletRegistrations().values()));
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return DispatcherServlet.class.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
|
||||
|
||||
private final AntPathRequestMatcher ant;
|
||||
|
||||
private final MvcRequestMatcher mvc;
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final RequestMatcher dispatcherServlet;
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
ServletContext servletContext) {
|
||||
this(ant, mvc, new OrRequestMatcher(new MockMvcRequestMatcher(),
|
||||
new DispatcherServletRequestMatcher(servletContext)));
|
||||
}
|
||||
|
||||
DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc,
|
||||
RequestMatcher dispatcherServlet) {
|
||||
this.ant = ant;
|
||||
this.mvc = mvc;
|
||||
this.servletContext = servletContext;
|
||||
this.dispatcherServlet = dispatcherServlet;
|
||||
}
|
||||
|
||||
RequestMatcher requestMatcher(HttpServletRequest request) {
|
||||
if (this.dispatcherServlet.matches(request)) {
|
||||
return this.mvc;
|
||||
}
|
||||
return this.ant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return this.mvc.matches(request);
|
||||
}
|
||||
return this.ant.matches(request);
|
||||
return requestMatcher(request).matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatchResult matcher(HttpServletRequest request) {
|
||||
String name = request.getHttpServletMapping().getServletName();
|
||||
ServletRegistration registration = this.servletContext.getServletRegistration(name);
|
||||
Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context");
|
||||
if (isDispatcherServlet(registration)) {
|
||||
return this.mvc.matcher(request);
|
||||
}
|
||||
return this.ant.matcher(request);
|
||||
}
|
||||
|
||||
private boolean isDispatcherServlet(ServletRegistration registration) {
|
||||
Class<?> dispatcherServlet = ClassUtils
|
||||
.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
|
||||
try {
|
||||
Class<?> clazz = Class.forName(registration.getClassName());
|
||||
return dispatcherServlet.isAssignableFrom(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
return requestMatcher(request).matcher(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-2
@@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web.reactive;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -30,6 +31,8 @@ import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
|
||||
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.reactive.result.view.AbstractView;
|
||||
@@ -65,8 +68,12 @@ class WebFluxSecurityConfiguration {
|
||||
|
||||
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
|
||||
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
|
||||
WebFilterChainProxy springSecurityWebFilterChainFilter() {
|
||||
return new WebFilterChainProxy(getSecurityWebFilterChains());
|
||||
WebFilterChainProxy springSecurityWebFilterChainFilter(ObjectProvider<ServerWebExchangeFirewall> firewall,
|
||||
ObjectProvider<ServerExchangeRejectedHandler> rejectedHandler) {
|
||||
WebFilterChainProxy webFilterChainProxy = new WebFilterChainProxy(getSecurityWebFilterChains());
|
||||
firewall.ifUnique(webFilterChainProxy::setFirewall);
|
||||
rejectedHandler.ifUnique(webFilterChainProxy::setExchangeRejectedHandler);
|
||||
return webFilterChainProxy;
|
||||
}
|
||||
|
||||
@Bean(name = AbstractView.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.config.http;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
@@ -301,7 +302,7 @@ class HttpConfigurationBuilder {
|
||||
|
||||
// Needed to account for placeholders
|
||||
static String createPath(String path, boolean lowerCase) {
|
||||
return lowerCase ? path.toLowerCase() : path;
|
||||
return lowerCase ? path.toLowerCase(Locale.ENGLISH) : path;
|
||||
}
|
||||
|
||||
BeanMetadataElement getSecurityContextHolderStrategyForAuthenticationFilters() {
|
||||
|
||||
+85
-8
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,27 +30,35 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.config.MockServletContext;
|
||||
import org.springframework.security.config.TestMockHttpServletMappings;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractRequestMatcherRegistry}.
|
||||
@@ -206,18 +214,65 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
mockMvcIntrospector(true);
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
given(this.context.getServletContext()).willReturn(servletContext);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(AntPathRequestMatcher.class);
|
||||
}
|
||||
|
||||
// gh-14418
|
||||
@Test
|
||||
public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherType() throws Exception {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
try (SpringTestContext spring = new SpringTestContext(this)) {
|
||||
spring.register(MockMvcConfiguration.class)
|
||||
.postProcessor((context) -> context.setServletContext(servletContext))
|
||||
.autowire();
|
||||
this.matcherRegistry.setApplicationContext(spring.getContext());
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(spring.getContext()).build();
|
||||
MockHttpServletRequest request = mvc.perform(get("/")).andReturn().getRequest();
|
||||
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class).addMapping("/one");
|
||||
servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two");
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
servletContext.addServlet("servletOne", Servlet.class);
|
||||
servletContext.addServlet("servletTwo", Servlet.class);
|
||||
requestMatchers = this.matcherRegistry.requestMatchers("/**");
|
||||
assertThat(requestMatchers).isNotEmpty();
|
||||
assertThat(requestMatchers).hasSize(1);
|
||||
assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class))
|
||||
.extracting((matcher) -> matcher.requestMatcher(request))
|
||||
.isInstanceOf(MvcRequestMatcher.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -345,6 +400,19 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
verifyNoMoreInteractions(mvc);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesWhenNoMappingThenException() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
servletContext.addServlet("default", DispatcherServlet.class).addMapping("/");
|
||||
servletContext.addServlet("path", Servlet.class).addMapping("/services/*");
|
||||
MvcRequestMatcher mvc = mock(MvcRequestMatcher.class);
|
||||
AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class);
|
||||
DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant,
|
||||
mvc, servletContext);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint");
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> requestMatcher.matcher(request));
|
||||
}
|
||||
|
||||
private void mockMvcIntrospector(boolean isPresent) {
|
||||
ApplicationContext context = this.matcherRegistry.getApplicationContext();
|
||||
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
|
||||
@@ -381,11 +449,13 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
return requestMatchers;
|
||||
}
|
||||
|
||||
private static List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
private List<RequestMatcher> unwrap(List<RequestMatcher> wrappedMatchers) {
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
for (RequestMatcher requestMatcher : wrappedMatchers) {
|
||||
if (requestMatcher instanceof AbstractRequestMatcherRegistry.DeferredRequestMatcher) {
|
||||
requestMatchers.add(((DeferredRequestMatcher) requestMatcher).requestMatcher);
|
||||
if (requestMatcher instanceof DeferredRequestMatcher) {
|
||||
DeferredRequestMatcher deferred = (DeferredRequestMatcher) requestMatcher;
|
||||
WebApplicationContext web = (WebApplicationContext) getApplicationContext();
|
||||
requestMatchers.add(deferred.requestMatcher(web.getServletContext()));
|
||||
}
|
||||
else {
|
||||
requestMatchers.add(requestMatcher);
|
||||
@@ -396,4 +466,11 @@ public class AbstractRequestMatcherRegistryTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class MockMvcConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -76,7 +76,7 @@ public class AuthorizeRequestsTests {
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.servletContext = spy(MockServletContext.mvc());
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(this.servletContext, "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -75,7 +75,7 @@ public class HttpSecuritySecurityMatchersTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -72,7 +72,7 @@ public class UrlAuthorizationConfigurerTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.request = new MockHttpServletRequest("GET", "");
|
||||
this.request = new MockHttpServletRequest(MockServletContext.mvc(), "GET", "");
|
||||
this.request.setMethod("GET");
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
|
||||
+78
@@ -16,14 +16,26 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.reactive;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
|
||||
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
|
||||
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
|
||||
import org.springframework.web.server.handler.DefaultWebFilterChain;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -47,6 +59,46 @@ public class WebFluxSecurityConfigurationTests {
|
||||
assertThat(webFilterChainProxy).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfigWhenDefaultThenFirewalled() throws Exception {
|
||||
this.spring
|
||||
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
|
||||
WebFluxSecurityConfiguration.class)
|
||||
.autowire();
|
||||
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
|
||||
DefaultWebFilterChain chain = emptyChain();
|
||||
webFilterChainProxy.filter(exchange, chain).block();
|
||||
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfigWhenCustomRejectedHandler() throws Exception {
|
||||
this.spring
|
||||
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
|
||||
WebFluxSecurityConfiguration.class, CustomServerExchangeRejectedHandlerConfig.class)
|
||||
.autowire();
|
||||
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
|
||||
DefaultWebFilterChain chain = emptyChain();
|
||||
webFilterChainProxy.filter(exchange, chain).block();
|
||||
assertThat(exchange.getResponse().getStatusCode())
|
||||
.isEqualTo(CustomServerExchangeRejectedHandlerConfig.EXPECTED_STATUS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfigWhenFirewallBeanThenCustomized() throws Exception {
|
||||
this.spring
|
||||
.register(ServerHttpSecurityConfiguration.class, ReactiveAuthenticationTestConfiguration.class,
|
||||
WebFluxSecurityConfiguration.class, NoOpFirewallConfig.class)
|
||||
.autowire();
|
||||
WebFilterChainProxy webFilterChainProxy = this.spring.getContext().getBean(WebFilterChainProxy.class);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/;/").build());
|
||||
DefaultWebFilterChain chain = emptyChain();
|
||||
webFilterChainProxy.filter(exchange, chain).block();
|
||||
assertThat(exchange.getResponse().getStatusCode()).isNotEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigWhenBeanProxyingEnabledAndSubclassThenWebFilterChainProxyExists() {
|
||||
this.spring
|
||||
@@ -57,6 +109,32 @@ public class WebFluxSecurityConfigurationTests {
|
||||
assertThat(webFilterChainProxy).isNotNull();
|
||||
}
|
||||
|
||||
private static @NotNull DefaultWebFilterChain emptyChain() {
|
||||
return new DefaultWebFilterChain((webExchange) -> Mono.empty(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NoOpFirewallConfig {
|
||||
|
||||
@Bean
|
||||
ServerWebExchangeFirewall noOpFirewall() {
|
||||
return ServerWebExchangeFirewall.INSECURE_NOOP;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomServerExchangeRejectedHandlerConfig {
|
||||
|
||||
static HttpStatus EXPECTED_STATUS = HttpStatus.I_AM_A_TEAPOT;
|
||||
|
||||
@Bean
|
||||
ServerExchangeRejectedHandler rejectedHandler() {
|
||||
return new HttpStatusExchangeRejectedHandler(EXPECTED_STATUS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class SubclassConfig extends WebFluxSecurityConfiguration {
|
||||
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* 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.
|
||||
@@ -79,10 +79,10 @@ public class SimpleAttributes2GrantedAuthoritiesMapper
|
||||
*/
|
||||
private GrantedAuthority getGrantedAuthority(String attribute) {
|
||||
if (isConvertAttributeToLowerCase()) {
|
||||
attribute = attribute.toLowerCase(Locale.getDefault());
|
||||
attribute = attribute.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
else if (isConvertAttributeToUpperCase()) {
|
||||
attribute = attribute.toUpperCase(Locale.getDefault());
|
||||
attribute = attribute.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
if (isAddPrefixIfAlreadyExisting() || !attribute.startsWith(getAttributePrefix())) {
|
||||
return new SimpleGrantedAuthority(getAttributePrefix() + attribute);
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.core.authority.mapping;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@@ -71,10 +72,10 @@ public final class SimpleAuthorityMapper implements GrantedAuthoritiesMapper, In
|
||||
|
||||
private GrantedAuthority mapAuthority(String name) {
|
||||
if (this.convertToUpperCase) {
|
||||
name = name.toUpperCase();
|
||||
name = name.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
else if (this.convertToLowerCase) {
|
||||
name = name.toLowerCase();
|
||||
name = name.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
if (this.prefix.length() > 0 && !name.startsWith(this.prefix)) {
|
||||
name = this.prefix + name;
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.core.userdetails;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -91,7 +92,7 @@ public class MapReactiveUserDetailsService implements ReactiveUserDetailsService
|
||||
}
|
||||
|
||||
private String getKey(String username) {
|
||||
return username.toLowerCase();
|
||||
return username.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@ package org.springframework.security.core.userdetails.memory;
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -45,10 +46,10 @@ public class UserAttributeEditor extends PropertyEditorSupport {
|
||||
userAttrib.setPassword(currentToken);
|
||||
}
|
||||
else {
|
||||
if (currentToken.toLowerCase().equals("enabled")) {
|
||||
if (currentToken.toLowerCase(Locale.ENGLISH).equals("enabled")) {
|
||||
userAttrib.setEnabled(true);
|
||||
}
|
||||
else if (currentToken.toLowerCase().equals("disabled")) {
|
||||
else if (currentToken.toLowerCase(Locale.ENGLISH).equals("disabled")) {
|
||||
userAttrib.setEnabled(false);
|
||||
}
|
||||
else {
|
||||
|
||||
+12
-1
@@ -75,7 +75,6 @@ public final class SecurityJackson2Modules {
|
||||
|
||||
private static final List<String> securityJackson2ModuleClasses = Arrays.asList(
|
||||
"org.springframework.security.jackson2.CoreJackson2Module",
|
||||
"org.springframework.security.cas.jackson2.CasJackson2Module",
|
||||
"org.springframework.security.web.jackson2.WebJackson2Module",
|
||||
"org.springframework.security.web.server.jackson2.WebServerJackson2Module");
|
||||
|
||||
@@ -89,6 +88,15 @@ public final class SecurityJackson2Modules {
|
||||
|
||||
private static final String saml2Jackson2ModuleClass = "org.springframework.security.saml2.jackson2.Saml2Jackson2Module";
|
||||
|
||||
private static final String casJackson2ModuleClass = "org.springframework.security.cas.jackson2.CasJackson2Module";
|
||||
|
||||
private static final boolean casJacksonPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = SecurityJackson2Modules.class.getClassLoader();
|
||||
casJacksonPresent = ClassUtils.isPresent(casJackson2ModuleClass, classLoader);
|
||||
}
|
||||
|
||||
private SecurityJackson2Modules() {
|
||||
}
|
||||
|
||||
@@ -140,6 +148,9 @@ public final class SecurityJackson2Modules {
|
||||
if (ClassUtils.isPresent(saml2Jackson2ModuleClass, loader)) {
|
||||
addToModulesList(loader, modules, saml2Jackson2ModuleClass);
|
||||
}
|
||||
if (casJacksonPresent) {
|
||||
addToModulesList(loader, modules, casJackson2ModuleClass);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.provisioning;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
@@ -96,23 +97,23 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
|
||||
@Override
|
||||
public void createUser(UserDetails user) {
|
||||
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
|
||||
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
|
||||
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(String username) {
|
||||
this.users.remove(username.toLowerCase());
|
||||
this.users.remove(username.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUser(UserDetails user) {
|
||||
Assert.isTrue(userExists(user.getUsername()), "user should exist");
|
||||
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
|
||||
this.users.put(user.getUsername().toLowerCase(Locale.ROOT), new MutableUser(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userExists(String username) {
|
||||
return this.users.containsKey(username.toLowerCase());
|
||||
return this.users.containsKey(username.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,14 +144,14 @@ public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetai
|
||||
@Override
|
||||
public UserDetails updatePassword(UserDetails user, String newPassword) {
|
||||
String username = user.getUsername();
|
||||
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
|
||||
MutableUserDetails mutableUser = this.users.get(username.toLowerCase(Locale.ROOT));
|
||||
mutableUser.setPassword(newPassword);
|
||||
return mutableUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserDetails user = this.users.get(username.toLowerCase());
|
||||
UserDetails user = this.users.get(username.toLowerCase(Locale.ROOT));
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.crypto.password;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
@@ -50,11 +51,11 @@ public class LdapShaPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final String SSHA_PREFIX = "{SSHA}";
|
||||
|
||||
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase();
|
||||
private static final String SSHA_PREFIX_LC = SSHA_PREFIX.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
private static final String SHA_PREFIX = "{SHA}";
|
||||
|
||||
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase();
|
||||
private static final String SHA_PREFIX_LC = SHA_PREFIX.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
private BytesKeyGenerator saltGenerator;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ urls:
|
||||
redirect_facility: httpd
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.11/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
|
||||
snapshot: true
|
||||
runtime:
|
||||
log:
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
***** xref:servlet/authentication/passwords/in-memory.adoc[In Memory]
|
||||
***** xref:servlet/authentication/passwords/jdbc.adoc[JDBC]
|
||||
***** xref:servlet/authentication/passwords/user-details.adoc[UserDetails]
|
||||
***** xref:servlet/authentication/passwords/credentials-container.adoc[CredentialsContainer]
|
||||
***** xref:servlet/authentication/passwords/user-details-service.adoc[UserDetailsService]
|
||||
***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
|
||||
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
||||
@@ -159,6 +160,7 @@
|
||||
*** xref:reactive/exploits/csrf.adoc[CSRF]
|
||||
*** xref:reactive/exploits/headers.adoc[Headers]
|
||||
*** xref:reactive/exploits/http.adoc[HTTP Requests]
|
||||
*** xref:reactive/exploits/firewall.adoc[]
|
||||
** Integrations
|
||||
*** xref:reactive/integrations/cors.adoc[CORS]
|
||||
*** xref:reactive/integrations/rsocket.adoc[RSocket]
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
[[webflux-serverwebexchangefirewall]]
|
||||
= ServerWebExchangeFirewall
|
||||
|
||||
There are various ways a request can be created by malicious users that can exploit applications.
|
||||
Spring Security provides the `ServerWebExchangeFirewall` to allow rejecting requests that look malicious.
|
||||
The default implementation is `StrictServerWebExchangeFirewall` which rejects malicious requests.
|
||||
|
||||
For example a request could contain path-traversal sequences (such as `/../`) or multiple forward slashes (`//`) that could also cause pattern-matches to fail.
|
||||
Some containers normalize these out before performing the servlet mapping, but others do not.
|
||||
To protect against issues like these, `WebFilterChainProxy` uses a `ServerWebExchangeFirewall` strategy to check and wrap the request.
|
||||
By default, un-normalized requests are automatically rejected, and path parameters are removed for matching purposes.
|
||||
(So, for example, an original request path of `/secure;hack=1/somefile.html;hack=2` is returned as `/secure/somefile.html`.)
|
||||
It is, therefore, essential that a `WebFilterChainProxy` is used.
|
||||
|
||||
In practice, we recommend that you use method security at your service layer, to control access to your application, rather than rely entirely on the use of security constraints defined at the web-application level.
|
||||
URLs change, and it is difficult to take into account all the possible URLs that an application might support and how requests might be manipulated.
|
||||
You should restrict yourself to using a few simple patterns that are simple to understand.
|
||||
Always try to use a "`deny-by-default`" approach, where you have a catch-all wildcard (`/**` or `**`) defined last to deny access.
|
||||
|
||||
Security defined at the service layer is much more robust and harder to bypass, so you should always take advantage of Spring Security's method security options.
|
||||
|
||||
You can customize the `ServerWebExchangeFirewall` by exposing it as a Bean.
|
||||
|
||||
.Allow Matrix Variables
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public StrictServerWebExchangeFirewall httpFirewall() {
|
||||
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
firewall.setAllowSemicolon(true);
|
||||
return firewall;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun httpFirewall(): StrictServerWebExchangeFirewall {
|
||||
val firewall = StrictServerWebExchangeFirewall()
|
||||
firewall.setAllowSemicolon(true)
|
||||
return firewall
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
To protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering], the `StrictServerWebExchangeFirewall` provides an allowed list of valid HTTP methods that are allowed.
|
||||
The default valid methods are `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, and `PUT`.
|
||||
If your application needs to modify the valid methods, you can configure a custom `StrictServerWebExchangeFirewall` bean.
|
||||
The following example allows only HTTP `GET` and `POST` methods:
|
||||
|
||||
|
||||
.Allow Only GET & POST
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public StrictServerWebExchangeFirewall httpFirewall() {
|
||||
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
|
||||
return firewall;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun httpFirewall(): StrictServerWebExchangeFirewall {
|
||||
val firewall = StrictServerWebExchangeFirewall()
|
||||
firewall.setAllowedHttpMethods(listOf("GET", "POST"))
|
||||
return firewall
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If you must allow any HTTP method (not recommended), you can use `StrictServerWebExchangeFirewall.setUnsafeAllowAnyHttpMethod(true)`.
|
||||
Doing so entirely disables validation of the HTTP method.
|
||||
|
||||
|
||||
[[webflux-serverwebexchangefirewall-headers-parameters]]
|
||||
`StrictServerWebExchangeFirewall` also checks header names and values and parameter names.
|
||||
It requires that each character have a defined code point and not be a control character.
|
||||
|
||||
This requirement can be relaxed or adjusted as necessary by using the following methods:
|
||||
|
||||
* `StrictServerWebExchangeFirewall#setAllowedHeaderNames(Predicate)`
|
||||
* `StrictServerWebExchangeFirewall#setAllowedHeaderValues(Predicate)`
|
||||
* `StrictServerWebExchangeFirewall#setAllowedParameterNames(Predicate)`
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Parameter values can be also controlled with `setAllowedParameterValues(Predicate)`.
|
||||
====
|
||||
|
||||
For example, to switch off this check, you can wire your `StrictServerWebExchangeFirewall` with `Predicate` instances that always return `true`:
|
||||
|
||||
.Allow Any Header Name, Header Value, and Parameter Name
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public StrictServerWebExchangeFirewall httpFirewall() {
|
||||
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
firewall.setAllowedHeaderNames((header) -> true);
|
||||
firewall.setAllowedHeaderValues((header) -> true);
|
||||
firewall.setAllowedParameterNames((parameter) -> true);
|
||||
return firewall;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun httpFirewall(): StrictServerWebExchangeFirewall {
|
||||
val firewall = StrictServerWebExchangeFirewall()
|
||||
firewall.setAllowedHeaderNames { true }
|
||||
firewall.setAllowedHeaderValues { true }
|
||||
firewall.setAllowedParameterNames { true }
|
||||
return firewall
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Alternatively, there might be a specific value that you need to allow.
|
||||
|
||||
For example, iPhone Xʀ uses a `User-Agent` that includes a character that is not in the ISO-8859-1 charset.
|
||||
Due to this fact, some application servers parse this value into two separate characters, the latter being an undefined character.
|
||||
|
||||
You can address this with the `setAllowedHeaderValues` method:
|
||||
|
||||
.Allow Certain User Agents
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public StrictServerWebExchangeFirewall httpFirewall() {
|
||||
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
Pattern allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
|
||||
Pattern userAgent = ...;
|
||||
firewall.setAllowedHeaderValues((header) -> allowed.matcher(header).matches() || userAgent.matcher(header).matches());
|
||||
return firewall;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun httpFirewall(): StrictServerWebExchangeFirewall {
|
||||
val firewall = StrictServerWebExchangeFirewall()
|
||||
val allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*")
|
||||
val userAgent = Pattern.compile(...)
|
||||
firewall.setAllowedHeaderValues { allowed.matcher(it).matches() || userAgent.matcher(it).matches() }
|
||||
return firewall
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
In the case of header values, you may instead consider parsing them as UTF-8 at verification time:
|
||||
|
||||
.Parse Headers As UTF-8
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
firewall.setAllowedHeaderValues((header) -> {
|
||||
String parsed = new String(header.getBytes(ISO_8859_1), UTF_8);
|
||||
return allowed.matcher(parsed).matches();
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
firewall.setAllowedHeaderValues {
|
||||
val parsed = String(header.getBytes(ISO_8859_1), UTF_8)
|
||||
return allowed.matcher(parsed).matches()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
The `ServerExchangeRejectedHandler` interface is used to handle `ServerExchangeRejectedException` throw by Spring Security's `ServerWebExchangeFirewall`.
|
||||
By default `HttpStatusExchangeRejectedHandler` is used to send an HTTP 400 response to clients when a request is rejected.
|
||||
To customize the behavior, users can expose a `ServerExchangeRejectedHandler` Bean.
|
||||
For example, the following will send an HTTP 404 when the request is rejected:
|
||||
|
||||
|
||||
.Send 404 on Request Rejected
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
ServerExchangeRejectedHandler rejectedHandler() {
|
||||
return new HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun rejectedHandler(): ServerExchangeRejectedHandler {
|
||||
return HttpStatusExchangeRejectedHandler(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Handling can be completely customized by creating a custom `ServerExchangeRejectedHandler` implementation.
|
||||
@@ -0,0 +1,12 @@
|
||||
[[servlet-authentication-credentialscontainer]]
|
||||
= CredentialsContainer
|
||||
|
||||
{security-api-url}org/springframework/security/core/CredentialsContainer.html[The `CredentialsContainer`] interface indicates that the implementing object contains sensitive data, and is used internally by Spring Security to erase the authentication credentials after a successful authentication.
|
||||
This interface is implemented by most of Spring Security internal domain classes, like {security-api-url}org/springframework/security/core/userdetails/User.html[User] and {security-api-url}org/springframework/security/authentication/UsernamePasswordAuthenticationToken.html[UsernamePasswordAuthenticationToken].
|
||||
|
||||
The `ProviderManager` manager checks whether the returned `Authentication` implements this interface.
|
||||
If so, xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager-erasing-credentials[it calls the `eraseCredentials` method] to remove the credentials from the object.
|
||||
|
||||
If you want your custom authentication objects to have their credentials erased after authentication, you should ensure that the classes implement the `CredentialsContainer` interface.
|
||||
|
||||
Users who are writing their own `AuthenticationProvider` implementations should create and return an appropriate `Authentication` object there, minus any sensitive data, rather than using this interface.
|
||||
@@ -326,156 +326,7 @@ Normally, Spring Security builds an `AuthenticationManager` internally composed
|
||||
In certain cases, it may still be desired to customize the instance of `AuthenticationManager` used by Spring Security.
|
||||
For example, you may need to simply disable xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager-erasing-credentials[credential erasure] for cached users.
|
||||
|
||||
The recommended way to do this is to simply publish your own `AuthenticationManager` bean, and Spring Security will use it.
|
||||
You can publish an `AuthenticationManager` using the following configuration:
|
||||
|
||||
.Publish `AuthenticationManager` bean for Spring Security
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/login").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.formLogin(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
UserDetailsService userDetailsService,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
|
||||
authenticationProvider.setUserDetailsService(userDetailsService);
|
||||
authenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
|
||||
ProviderManager providerManager = new ProviderManager(authenticationProvider);
|
||||
providerManager.setEraseCredentialsAfterAuthentication(false);
|
||||
|
||||
return providerManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
|
||||
return new InMemoryUserDetailsManager(userDetails);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
<intercept-url pattern="/login" access="permitAll"/>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<form-login />
|
||||
<http-basic />
|
||||
|
||||
<bean id="authenticationManager"
|
||||
class="org.springframework.security.authentication.ProviderManager">
|
||||
<constructor-arg>
|
||||
<bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
|
||||
<property name="userDetailsService" ref="userDetailsService" />
|
||||
<property name="passwordEncoder" ref="passwordEncoder" />
|
||||
</bean>
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
|
||||
<user-service id="userDetailsService">
|
||||
<user name="user"
|
||||
password="{noop}password"
|
||||
authorities="ROLE_USER" />
|
||||
</user-service>
|
||||
|
||||
<bean id="passwordEncoder"
|
||||
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
|
||||
</http>
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/login", permitAll)
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
httpBasic { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun authenticationManager(
|
||||
userDetailsService: UserDetailsService,
|
||||
passwordEncoder: PasswordEncoder): AuthenticationManager {
|
||||
val authenticationProvider = DaoAuthenticationProvider()
|
||||
authenticationProvider.setUserDetailsService(userDetailsService)
|
||||
authenticationProvider.setPasswordEncoder(passwordEncoder)
|
||||
|
||||
val providerManager = ProviderManager(authenticationProvider)
|
||||
providerManager.eraseCredentialsAfterAuthentication = false
|
||||
|
||||
return providerManager
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
val user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
|
||||
return InMemoryUserDetailsManager(user)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
=====
|
||||
|
||||
Alternatively, you can take advantage of the fact that the `AuthenticationManagerBuilder` used to build Spring Security's global `AuthenticationManager` is published as a bean.
|
||||
To do this, you can take advantage of the fact that the `AuthenticationManagerBuilder` used to build Spring Security's global `AuthenticationManager` is published as a bean.
|
||||
You can configure the builder as follows:
|
||||
|
||||
.Configure global `AuthenticationManagerBuilder`
|
||||
@@ -539,3 +390,142 @@ class SecurityConfig {
|
||||
}
|
||||
----
|
||||
=====
|
||||
|
||||
Alternatively, you may configure a local `AuthenticationManager` to override the global one.
|
||||
|
||||
.Configure local `AuthenticationManager` for Spring Security
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.authenticationManager(authenticationManager());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private AuthenticationManager authenticationManager() {
|
||||
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
|
||||
authenticationProvider.setUserDetailsService(userDetailsService());
|
||||
authenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||
|
||||
ProviderManager providerManager = new ProviderManager(authenticationProvider);
|
||||
providerManager.setEraseCredentialsAfterAuthentication(false);
|
||||
|
||||
return providerManager;
|
||||
}
|
||||
|
||||
private UserDetailsService userDetailsService() {
|
||||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
|
||||
return new InMemoryUserDetailsManager(userDetails);
|
||||
}
|
||||
|
||||
private PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http authentication-manager-ref="authenticationManager">
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<form-login />
|
||||
<http-basic />
|
||||
|
||||
<bean id="authenticationManager"
|
||||
class="org.springframework.security.authentication.ProviderManager">
|
||||
<constructor-arg>
|
||||
<bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
|
||||
<property name="userDetailsService" ref="userDetailsService" />
|
||||
<property name="passwordEncoder" ref="passwordEncoder" />
|
||||
</bean>
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
|
||||
<user-service id="userDetailsService">
|
||||
<user name="user"
|
||||
password="{noop}password"
|
||||
authorities="ROLE_USER" />
|
||||
</user-service>
|
||||
|
||||
<bean id="passwordEncoder"
|
||||
class="org.springframework.security.crypto.factory.PasswordEncoderFactories" factory-method="createDelegatingPasswordEncoder"/>
|
||||
</http>
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
httpBasic { }
|
||||
authenticationManager = authenticationManager()
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun authenticationManager(): AuthenticationManager {
|
||||
val authenticationProvider = DaoAuthenticationProvider()
|
||||
authenticationProvider.setUserDetailsService(userDetailsService())
|
||||
authenticationProvider.setPasswordEncoder(passwordEncoder())
|
||||
|
||||
val providerManager = ProviderManager(authenticationProvider)
|
||||
providerManager.eraseCredentialsAfterAuthentication = false
|
||||
|
||||
return providerManager
|
||||
}
|
||||
|
||||
private fun userDetailsService(): UserDetailsService {
|
||||
val user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
|
||||
return InMemoryUserDetailsManager(user)
|
||||
}
|
||||
|
||||
private fun passwordEncoder(): PasswordEncoder {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
=====
|
||||
|
||||
|
||||
@@ -10,6 +10,41 @@ However, despite leveraging a username/password for authentication it does not i
|
||||
There are many different scenarios for how an LDAP server may be configured so Spring Security's LDAP provider is fully configurable.
|
||||
It uses separate strategy interfaces for authentication and role retrieval and provides default implementations which can be configured to handle a wide range of situations.
|
||||
|
||||
[[servlet-authentication-ldap-required-dependencies]]
|
||||
== Required Dependencies
|
||||
|
||||
To get started, add the `spring-security-ldap` dependency to your project.
|
||||
When using Spring Boot, add the following dependencies:
|
||||
|
||||
.Spring Security LDAP Dependencies
|
||||
[tabs]
|
||||
======
|
||||
Maven::
|
||||
+
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-ldap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-ldap</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
Gradle::
|
||||
+
|
||||
[source,groovy,role="secondary"]
|
||||
----
|
||||
depenendencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-ldap"
|
||||
implementation "org.springframework.security:spring-security-ldap"
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[servlet-authentication-ldap-prerequisites]]
|
||||
== Prerequisites
|
||||
|
||||
@@ -81,14 +116,14 @@ dn: cn=user,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: user
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=user,ou=people,dc=springframework,dc=org
|
||||
|
||||
dn: cn=admin,ou=groups,dc=springframework,dc=org
|
||||
objectclass: top
|
||||
objectclass: groupOfNames
|
||||
cn: admin
|
||||
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
|
||||
member: uid=admin,ou=people,dc=springframework,dc=org
|
||||
----
|
||||
|
||||
[[servlet-authentication-ldap-unboundid]]
|
||||
|
||||
@@ -52,9 +52,9 @@ If you have more than one in your application context, you need to specify which
|
||||
|
||||
[[remember-me-persistent-token]]
|
||||
== Persistent Token Approach
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[http://jaspan.com/improved_persistent_login_cookie_best_practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
This approach is based on the article https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice[Improved Persistent Login Cookie Best Practice] with some minor modifications footnote:[Essentially, the username is not included in the cookie, to prevent exposing a valid login name unecessarily.
|
||||
There is a discussion on this in the comments section of this article.].
|
||||
To use the this approach with namespace configuration, you would supply a datasource reference:
|
||||
To use this approach with namespace configuration, you would supply a datasource reference:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
|
||||
@@ -339,7 +339,7 @@ Java::
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
|
||||
JWTProcessor jwtProcessor(JWTClaimsSetAwareJWSKeySelector keySelector) {
|
||||
ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
|
||||
new DefaultJWTProcessor();
|
||||
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.4",
|
||||
"antora": "3.2.0-alpha.6",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@antora/collector-extension": "1.0.0-beta.5",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.10.0",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +56,8 @@
|
||||
|
||||
<!-- Lambdas that we can't replace with a method reference because a closure is required -->
|
||||
<suppress files="BearerTokenAuthenticationFilter\.java" checks="SpringLambda"/>
|
||||
|
||||
<!-- Ignore String.toUpperCase() and String.toLowerCase() checks in tests -->
|
||||
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
|
||||
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
|
||||
</suppressions>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.csrf.CsrfTokenAssert.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.AntPathRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.RegexRequestMatcher.*" />
|
||||
<property name="avoidStaticImportExcludes" value="org.assertj.core.api.InstanceOfAssertFactories.*"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
|
||||
@@ -28,5 +29,21 @@
|
||||
<property name="message" value="Please use assertThatExceptionOfType." />
|
||||
<property name="ignoreComments" value="true" />
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
|
||||
<property name="id" value="toLowerCaseWithoutLocale"/>
|
||||
<property name="format" value="\.toLowerCase\(\)"/>
|
||||
<property name="maximum" value="0"/>
|
||||
<property name="message"
|
||||
value="String.toLowerCase() should be String.toLowerCase(Locale.ROOT) or String.toLowerCase(Locale.ENGLISH)"/>
|
||||
<property name="ignoreComments" value="true"/>
|
||||
</module>
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
|
||||
<property name="id" value="toUpperCaseWithoutLocale"/>
|
||||
<property name="format" value="\.toUpperCase\(\)"/>
|
||||
<property name="maximum" value="0"/>
|
||||
<property name="message"
|
||||
value="String.toUpperCase() should be String.toUpperCase(Locale.ROOT) or String.toUpperCase(Locale.ENGLISH)"/>
|
||||
<property name="ignoreComments" value="true"/>
|
||||
</module>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
@@ -12,4 +12,7 @@
|
||||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
<module name="SuppressWithPlainTextCommentFilter"/>
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value=".*[\\/]node(_modules|js)[\\/].*$"/>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
springBootVersion=2.7.12
|
||||
version=5.8.13-SNAPSHOT
|
||||
version=5.8.16
|
||||
samplesBranch=5.8.x
|
||||
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -7,7 +7,7 @@ io-spring-nohttp = "0.0.11"
|
||||
org-apache-directory-server = "1.5.5"
|
||||
org-aspectj = "1.9.20.1"
|
||||
org-bouncycastle = "1.70"
|
||||
org-eclipse-jetty = "9.4.54.v20240208"
|
||||
org-eclipse-jetty = "9.4.56.v20240826"
|
||||
org-jetbrains-kotlin = "1.7.22"
|
||||
org-jetbrains-kotlinx = "1.6.4"
|
||||
org-junit-jupiter = "5.9.3"
|
||||
@@ -15,7 +15,7 @@ org-mockito = "4.8.1"
|
||||
org-opensaml4 = "4.1.0"
|
||||
org-opensaml3 = "3.4.6"
|
||||
org-slf4j = "1.7.36"
|
||||
org-springframework = "5.3.34"
|
||||
org-springframework = "5.3.39"
|
||||
|
||||
[libraries]
|
||||
ch-qos-logback-logback-classic = "ch.qos.logback:logback-classic:1.2.13"
|
||||
@@ -30,9 +30,9 @@ com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:4.0.14"
|
||||
commons-collections = "commons-collections:commons-collections:3.2.2"
|
||||
io-freefair-gradle-aspectj-plugin = "io.freefair.gradle:aspectj-plugin:6.5.1"
|
||||
io-mockk = "io.mockk:mockk:1.13.3"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.44"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.43"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.9.RELEASE"
|
||||
io-projectreactor-netty-reactor-netty = "io.projectreactor.netty:reactor-netty:1.0.48"
|
||||
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.47"
|
||||
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.10.RELEASE"
|
||||
io-r2dbc-r2dbc-h2 = { module = "io.r2dbc:r2dbc-h2", version.ref = "io-r2dbc" }
|
||||
io-r2dbc-r2dbc-spi-test = { module = "io.r2dbc:r2dbc-spi-test", version.ref = "io-r2dbc" }
|
||||
io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" }
|
||||
@@ -69,7 +69,7 @@ org-eclipse-jetty-jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet",
|
||||
org-eclipse-persistence-javax-persistence = "org.eclipse.persistence:javax.persistence:2.2.1"
|
||||
org-hamcrest = "org.hamcrest:hamcrest:2.2"
|
||||
org-hibernate-hibernate-entitymanager = "org.hibernate:hibernate-entitymanager:5.6.15.Final"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.2"
|
||||
org-hsqldb = "org.hsqldb:hsqldb:2.7.4"
|
||||
org-jasig-cas-client-cas-client-core = "org.jasig.cas.client:cas-client-core:3.6.4"
|
||||
org-jetbrains-kotlin-kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "org-jetbrains-kotlin" }
|
||||
org-jetbrains-kotlin-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" }
|
||||
@@ -87,11 +87,11 @@ org-python-jython = "org.python:jython:2.5.3"
|
||||
org-seleniumhq-selenium-htmlunit-driver = "org.seleniumhq.selenium:htmlunit-driver:2.65.0"
|
||||
org-seleniumhq-selenium-selenium-java = "org.seleniumhq.selenium:selenium-java:3.141.59"
|
||||
org-seleniumhq-selenium-selenium-support = "org.seleniumhq.selenium:selenium-support:3.141.59"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.1"
|
||||
org-skyscreamer-jsonassert = "org.skyscreamer:jsonassert:1.5.3"
|
||||
org-slf4j-log4j-over-slf4j = { module = "org.slf4j:log4j-over-slf4j", version.ref = "org-slf4j" }
|
||||
org-slf4j-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "org-slf4j" }
|
||||
org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2021.2.18"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:2.4.1"
|
||||
org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:2.4.4"
|
||||
org-springframework-spring-framework-bom = { module = "org.springframework:spring-framework-bom", version.ref = "org-springframework" }
|
||||
org-synchronoss-cloud-nio-multipart-parser = "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
|
||||
|
||||
@@ -103,7 +103,7 @@ org-apache-commons-commons-io = "org.apache.commons:commons-io:1.3.2"
|
||||
io-github-gradle-nexus-publish-plugin = "io.github.gradle-nexus:publish-plugin:1.1.0"
|
||||
org-gretty-gretty = "org.gretty:gretty:3.0.9"
|
||||
com-github-ben-manes-gradle-versions-plugin = "com.github.ben-manes:gradle-versions-plugin:0.38.0"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.11"
|
||||
com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:compiler:0.9.14"
|
||||
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.29.4"
|
||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2005-2010 the original author or authors.
|
||||
* Copyright 2005-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.ldap;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.ldap.BadLdapGrammarException;
|
||||
|
||||
/**
|
||||
@@ -72,7 +74,7 @@ final class LdapEncoder {
|
||||
}
|
||||
|
||||
protected static String toTwoCharHex(char c) {
|
||||
String raw = Integer.toHexString(c).toUpperCase();
|
||||
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
|
||||
return (raw.length() > 1) ? raw : "0" + raw;
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2005-2010 the original author or authors.
|
||||
* Copyright 2005-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.ldap.authentication;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.ldap.BadLdapGrammarException;
|
||||
|
||||
/**
|
||||
@@ -72,7 +74,7 @@ final class LdapEncoder {
|
||||
}
|
||||
|
||||
protected static String toTwoCharHex(char c) {
|
||||
String raw = Integer.toHexString(c).toUpperCase();
|
||||
String raw = Integer.toHexString(c).toUpperCase(Locale.ENGLISH);
|
||||
return (raw.length() > 1) ? raw : "0" + raw;
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* 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.
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -136,15 +137,17 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
|
||||
ContextFactory contextFactory = new ContextFactory();
|
||||
|
||||
/**
|
||||
* @param domain the domain name (may be null or empty)
|
||||
* @param url an LDAP url (or multiple URLs)
|
||||
* @param rootDn the root DN (may be null or empty)
|
||||
* @param domain the domain name (can be null or empty)
|
||||
* @param url an LDAP url (or multiple space-delimited URLs).
|
||||
* @param rootDn the root DN (can be null or empty)
|
||||
* @see <a href="https://docs.oracle.com/javase/jndi/tutorial/ldap/misc/url.html">JNDI
|
||||
* URL format documentation</a>
|
||||
*/
|
||||
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) {
|
||||
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
|
||||
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
|
||||
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
|
||||
this.url = url;
|
||||
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null;
|
||||
this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase(Locale.ROOT) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +156,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
|
||||
*/
|
||||
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
|
||||
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
|
||||
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
|
||||
this.domain = StringUtils.hasText(domain) ? domain.toLowerCase(Locale.ROOT) : null;
|
||||
this.url = url;
|
||||
this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null;
|
||||
}
|
||||
@@ -362,7 +365,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
|
||||
}
|
||||
|
||||
String createBindPrincipal(String username) {
|
||||
if (this.domain == null || username.toLowerCase().endsWith(this.domain)) {
|
||||
if (this.domain == null || username.toLowerCase(Locale.ROOT).endsWith(this.domain)) {
|
||||
return username;
|
||||
}
|
||||
return username + "@" + this.domain;
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
@@ -179,7 +180,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
return null;
|
||||
}
|
||||
if (this.convertToUpperCase) {
|
||||
role = role.toUpperCase();
|
||||
role = role.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role);
|
||||
};
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NameNotFoundException;
|
||||
@@ -124,7 +125,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
|
||||
NamingEnumeration<?> ne = roleAttr.getAll();
|
||||
Object group = ne.next();
|
||||
String role = group.toString();
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase());
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase(Locale.ROOT));
|
||||
};
|
||||
|
||||
private String[] attributesToRetrieve;
|
||||
@@ -287,7 +288,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
|
||||
*/
|
||||
protected DistinguishedName buildGroupDn(String group) {
|
||||
DistinguishedName dn = new DistinguishedName(this.groupSearchBase);
|
||||
dn.add(this.groupRoleAttributeName, group.toLowerCase());
|
||||
dn.add(this.groupRoleAttributeName, group.toLowerCase(Locale.ROOT));
|
||||
return dn;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.ldap.userdetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -127,7 +128,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
|
||||
protected GrantedAuthority createAuthority(Object role) {
|
||||
if (role instanceof String) {
|
||||
if (this.convertToUpperCase) {
|
||||
role = ((String) role).toUpperCase();
|
||||
role = ((String) role).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
return new SimpleGrantedAuthority(this.rolePrefix + role);
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.ldap.userdetails;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -193,7 +194,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
|
||||
}
|
||||
for (String role : roles) {
|
||||
if (isConvertToUpperCase()) {
|
||||
role = role.toUpperCase();
|
||||
role = role.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
role = getRolePrefix() + role;
|
||||
// if the group already exist, we will not search for it's parents again.
|
||||
|
||||
+9
-8
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.messaging.web.csrf;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.security.crypto.codec.Utf8;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Copied from
|
||||
@@ -43,26 +44,26 @@ final class XorCsrfTokenUtils {
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+69
-1
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.messaging.web.csrf;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -141,6 +142,73 @@ public class XorCsrfChannelInterceptorTests {
|
||||
this.interceptor.preSend(message(), this.channel);
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsShorterThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenCsrfBytesIsLongerThanRandomBytesThenThrowsInvalidCsrfTokenException() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), actualToken);
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsShorterThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "a");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenTokenBytesIsLongerThanActualBytesThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("header", "param", "abcde");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), csrfToken);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void preSendWhenActualBytesIsEmptyThenThrowsInvalidCsrfTokenException() {
|
||||
this.messageHeaders.setNativeHeader(this.token.getHeaderName(), "");
|
||||
this.messageHeaders.getSessionAttributes().put(CsrfToken.class.getName(), this.token);
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(InvalidCsrfTokenException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(message(), mock(MessageChannel.class)));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Message<String> message() {
|
||||
return MessageBuilder.withPayload("message").copyHeaders(this.messageHeaders.toMap()).build();
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
@@ -753,7 +754,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
// @formatter:off
|
||||
return Stream.of(wwwAuthenticateHeader)
|
||||
.filter((header) -> !StringUtils.isEmpty(header))
|
||||
.filter((header) -> header.toLowerCase().startsWith("bearer"))
|
||||
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
|
||||
.map((header) -> header.substring("bearer".length()))
|
||||
.map((header) -> header.split(","))
|
||||
.flatMap(Stream::of)
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -735,7 +736,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
private Map<String, String> parseAuthParameters(String wwwAuthenticateHeader) {
|
||||
// @formatter:off
|
||||
return Stream.of(wwwAuthenticateHeader).filter((header) -> !StringUtils.isEmpty(header))
|
||||
.filter((header) -> header.toLowerCase().startsWith("bearer"))
|
||||
.filter((header) -> header.toLowerCase(Locale.ENGLISH).startsWith("bearer"))
|
||||
.map((header) -> header.substring("bearer".length()))
|
||||
.map((header) -> header.split(","))
|
||||
.flatMap(Stream::of)
|
||||
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -94,14 +94,10 @@ public final class WebSessionServerOAuth2AuthorizedClientRepository implements S
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, OAuth2AuthorizedClient> getAuthorizedClients(WebSession session) {
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = (session != null)
|
||||
? (Map<String, OAuth2AuthorizedClient>) session.getAttribute(this.sessionAttributeName) : null;
|
||||
if (authorizedClients == null) {
|
||||
authorizedClients = new HashMap<>();
|
||||
}
|
||||
return authorizedClients;
|
||||
Assert.notNull(session, "session cannot be null");
|
||||
Map<String, OAuth2AuthorizedClient> authorizedClients = session.getAttribute(this.sessionAttributeName);
|
||||
return (authorizedClients != null) ? authorizedClients : new HashMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.oauth2.client.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
@@ -24,10 +25,12 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -202,4 +205,28 @@ public class WebSessionServerOAuth2AuthorizedClientRepositoryTests {
|
||||
assertThat(loadedAuthorizedClient2).isSameAs(authorizedClient2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration1, this.principalName1,
|
||||
mock(OAuth2AccessToken.class));
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAuthorizedClientWhenSessionIsNullThenThrowIllegalArgumentException() {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
given(exchange.getSession()).willReturn(Mono.empty());
|
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, exchange).block())
|
||||
.withMessage("session cannot be null");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -463,7 +463,21 @@ public final class OAuth2AuthorizationRequest implements Serializable {
|
||||
Map<String, Object> parameters = getParameters(); // Not encoded
|
||||
this.parametersConsumer.accept(parameters);
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
parameters.forEach((k, v) -> queryParams.set(encodeQueryParam(k), encodeQueryParam(String.valueOf(v)))); // Encoded
|
||||
parameters.forEach((k, v) -> {
|
||||
String key = encodeQueryParam(k);
|
||||
if (v instanceof Iterable) {
|
||||
((Iterable<?>) v).forEach((value) -> queryParams.add(key, encodeQueryParam(String.valueOf(value))));
|
||||
}
|
||||
else if (v != null && v.getClass().isArray()) {
|
||||
Object[] values = (Object[]) v;
|
||||
for (Object value : values) {
|
||||
queryParams.add(key, encodeQueryParam(String.valueOf(value)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
queryParams.set(key, encodeQueryParam(String.valueOf(v)));
|
||||
}
|
||||
});
|
||||
UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri).queryParams(queryParams);
|
||||
return this.authorizationRequestUriFunction.apply(uriBuilder).toString();
|
||||
}
|
||||
|
||||
+47
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.core.endpoint;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -364,4 +365,49 @@ public class OAuth2AuthorizationRequestTests {
|
||||
+ "item%20amount=19.95%E2%82%AC&%C3%A2ge=4%C2%BD&item%20name=H%C3%85M%C3%96");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAdditionalParametersContainsArrayThenProperlyEncoded() {
|
||||
Map<String, Object> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("item1", new String[] { "1", "2" });
|
||||
additionalParameters.put("item2", "value2");
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&"
|
||||
+ "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&"
|
||||
+ "item1=1&item1=2&item2=value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAdditionalParametersContainsIterableThenProperlyEncoded() {
|
||||
Map<String, Object> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("item1", Arrays.asList("1", "2"));
|
||||
additionalParameters.put("item2", "value2");
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&"
|
||||
+ "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&"
|
||||
+ "item1=1&item1=2&item2=value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAdditionalParametersContainsNullThenAuthorizationRequestUriContainsNull() {
|
||||
Map<String, Object> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("item1", null);
|
||||
additionalParameters.put("item2", "value2");
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://example.com/login/oauth/authorize?response_type=code&client_id=client-id&state=state&"
|
||||
+ "redirect_uri=https://example.com/authorize/oauth2/code/registration-id&"
|
||||
+ "item1=null&item2=value2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-5
@@ -50,11 +50,6 @@ import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
* To locate the bearer token, this looks in the Reactor {@link Context} for a key of type
|
||||
* {@link Authentication}.
|
||||
*
|
||||
* Registering
|
||||
* {@see org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration.OAuth2ResourceServerWebFluxSecurityConfiguration.BearerRequestContextSubscriberRegistrar},
|
||||
* as a {@code @Bean} will take care of this automatically, but certainly an application
|
||||
* can supply a {@link Context} of its own to override.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.2
|
||||
*/
|
||||
|
||||
@@ -96,6 +96,7 @@ project.tasks.matching { t -> t.name == "sourcesJar"}.configureEach {
|
||||
|
||||
|
||||
javadoc {
|
||||
classpath += sourceSets.opensaml3Main.runtimeClasspath + sourceSets.opensaml4Main.runtimeClasspath
|
||||
source += sourceSets.opensaml3Main.allJava + sourceSets.opensaml4Main.allJava
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -45,8 +45,8 @@ public final class RelyingPartyRegistrations {
|
||||
* Return a {@link RelyingPartyRegistration.Builder} based off of the given SAML 2.0
|
||||
* Asserting Party (IDP) metadata location.
|
||||
*
|
||||
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
|
||||
* valid endpoints might include:
|
||||
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
|
||||
* Some valid endpoints might include:
|
||||
*
|
||||
* <pre>
|
||||
* metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
@@ -69,8 +69,8 @@ public final class RelyingPartyRegistrations {
|
||||
* about the asserting party. Thus, you will need to remember to still populate
|
||||
* anything about the relying party, like any private keys the relying party will use
|
||||
* for signing AuthnRequests.
|
||||
* @param metadataLocation The classpath- or file-based locations or HTTP endpoints of
|
||||
* the asserting party metadata file
|
||||
* @param metadataLocation The classpath- or file-based locations or HTTPS endpoints
|
||||
* of the asserting party metadata file
|
||||
* @return the {@link RelyingPartyRegistration.Builder} for further configuration
|
||||
*/
|
||||
public static RelyingPartyRegistration.Builder fromMetadataLocation(String metadataLocation) {
|
||||
@@ -130,8 +130,8 @@ public final class RelyingPartyRegistrations {
|
||||
* Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
|
||||
* of the given SAML 2.0 Asserting Party (IDP) metadata location.
|
||||
*
|
||||
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
|
||||
* valid endpoints might include:
|
||||
* Valid locations can be classpath- or file-based or they can be HTTPS endpoints.
|
||||
* Some valid endpoints might include:
|
||||
*
|
||||
* <pre>
|
||||
* metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
@@ -155,7 +155,7 @@ public final class RelyingPartyRegistrations {
|
||||
* about the asserting party. Thus, you will need to remember to still populate
|
||||
* anything about the relying party, like any private keys the relying party will use
|
||||
* for signing AuthnRequests.
|
||||
* @param location The classpath- or file-based locations or HTTP endpoints of the
|
||||
* @param location The classpath- or file-based locations or HTTPS endpoints of the
|
||||
* asserting party metadata file
|
||||
* @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
|
||||
* further configuration
|
||||
|
||||
+6
-2
@@ -5,8 +5,12 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.gradle.develocity" version "3.17.2"
|
||||
id "io.spring.ge.conventions" version "0.0.17"
|
||||
id "com.gradle.develocity" version "3.17.6"
|
||||
id "io.spring.ge.conventions" version "0.0.17" apply false
|
||||
}
|
||||
|
||||
if (System.getenv("DEVELOCITY_ACCESS_KEY")) {
|
||||
apply plugin: "io.spring.ge.conventions"
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004-2022 the original author or authors.
|
||||
* Copyright 2004-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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.taglibs.authz;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
@@ -169,7 +170,7 @@ public abstract class AbstractAuthorizeTag {
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
this.method = (method != null) ? method.toUpperCase() : null;
|
||||
this.method = (method != null) ? method.toUpperCase(Locale.ENGLISH) : null;
|
||||
}
|
||||
|
||||
private SecurityContext getContext() {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.web;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
@@ -45,7 +47,7 @@ public class PortResolverImpl implements PortResolver {
|
||||
@Override
|
||||
public int getServerPort(ServletRequest request) {
|
||||
int serverPort = request.getServerPort();
|
||||
String scheme = request.getScheme().toLowerCase();
|
||||
String scheme = request.getScheme().toLowerCase(Locale.ENGLISH);
|
||||
Integer mappedPort = getMappedPort(serverPort, scheme);
|
||||
return (mappedPort != null) ? mappedPort : serverPort;
|
||||
}
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's
|
||||
* <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* {@link RememberMeServices} implementation based on Barry Jaspan's <a href=
|
||||
* "https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved
|
||||
* Persistent Login Cookie Best Practice</a>.
|
||||
*
|
||||
* There is a slight modification to the described approach, in that the username is not
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -84,17 +84,16 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
@@ -114,9 +113,10 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+12
-3
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -74,6 +74,9 @@ public class CookieRequestCache implements RequestCache {
|
||||
return null;
|
||||
}
|
||||
String originalURI = decodeCookie(savedRequestCookie.getValue());
|
||||
if (originalURI == null) {
|
||||
return null;
|
||||
}
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(originalURI).build();
|
||||
DefaultSavedRequest.Builder builder = new DefaultSavedRequest.Builder();
|
||||
int port = getPort(uriComponents);
|
||||
@@ -123,8 +126,14 @@ public class CookieRequestCache implements RequestCache {
|
||||
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
|
||||
}
|
||||
|
||||
private static String decodeCookie(String encodedCookieValue) {
|
||||
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
|
||||
private String decodeCookie(String encodedCookieValue) {
|
||||
try {
|
||||
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
this.logger.debug("Failed decode cookie value " + encodedCookieValue);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getCookiePath(HttpServletRequest request) {
|
||||
|
||||
@@ -22,6 +22,12 @@ import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.web.server.firewall.HttpStatusExchangeRejectedHandler;
|
||||
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
|
||||
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
|
||||
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
|
||||
import org.springframework.security.web.server.firewall.StrictServerWebExchangeFirewall;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
@@ -38,6 +44,10 @@ public class WebFilterChainProxy implements WebFilter {
|
||||
|
||||
private final List<SecurityWebFilterChain> filters;
|
||||
|
||||
private ServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
|
||||
private ServerExchangeRejectedHandler exchangeRejectedHandler = new HttpStatusExchangeRejectedHandler();
|
||||
|
||||
public WebFilterChainProxy(List<SecurityWebFilterChain> filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
@@ -48,14 +58,43 @@ public class WebFilterChainProxy implements WebFilter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.firewall.getFirewalledExchange(exchange)
|
||||
.flatMap((firewalledExchange) -> filterFirewalledExchange(firewalledExchange, chain))
|
||||
.onErrorResume(ServerExchangeRejectedException.class,
|
||||
(rejected) -> this.exchangeRejectedHandler.handle(exchange, rejected).then(Mono.empty()));
|
||||
}
|
||||
|
||||
private Mono<Void> filterFirewalledExchange(ServerWebExchange firewalledExchange, WebFilterChain chain) {
|
||||
return Flux.fromIterable(this.filters)
|
||||
.filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange))
|
||||
.filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(firewalledExchange))
|
||||
.next()
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.switchIfEmpty(chain.filter(firewalledExchange).then(Mono.empty()))
|
||||
.flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
|
||||
.map((filters) -> new FilteringWebHandler(chain::filter, filters))
|
||||
.map(DefaultWebFilterChain::new)
|
||||
.flatMap((securedChain) -> securedChain.filter(exchange));
|
||||
.flatMap((securedChain) -> securedChain.filter(firewalledExchange));
|
||||
}
|
||||
|
||||
/**
|
||||
* Protects the application using the provided
|
||||
* {@link StrictServerWebExchangeFirewall}.
|
||||
* @param firewall the {@link StrictServerWebExchangeFirewall} to use. Cannot be null.
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public void setFirewall(ServerWebExchangeFirewall firewall) {
|
||||
Assert.notNull(firewall, "firewall cannot be null");
|
||||
this.firewall = firewall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles {@link ServerExchangeRejectedException} when the
|
||||
* {@link ServerWebExchangeFirewall} rejects the provided {@link ServerWebExchange}.
|
||||
* @param exchangeRejectedHandler the {@link ServerExchangeRejectedHandler} to use.
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public void setExchangeRejectedHandler(ServerExchangeRejectedHandler exchangeRejectedHandler) {
|
||||
Assert.notNull(exchangeRejectedHandler, "exchangeRejectedHandler cannot be null");
|
||||
this.exchangeRejectedHandler = exchangeRejectedHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -77,17 +77,16 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
|
||||
byte[] tokenBytes = Utf8.encode(token);
|
||||
int tokenSize = tokenBytes.length;
|
||||
if (actualBytes.length < tokenSize) {
|
||||
if (actualBytes.length != tokenSize * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract token and random bytes
|
||||
int randomBytesSize = actualBytes.length - tokenSize;
|
||||
byte[] xoredCsrf = new byte[tokenSize];
|
||||
byte[] randomBytes = new byte[randomBytesSize];
|
||||
byte[] randomBytes = new byte[tokenSize];
|
||||
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize);
|
||||
System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, 0, randomBytes, 0, tokenSize);
|
||||
System.arraycopy(actualBytes, tokenSize, xoredCsrf, 0, tokenSize);
|
||||
|
||||
byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf);
|
||||
return Utf8.decode(csrfBytes);
|
||||
@@ -107,9 +106,10 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT
|
||||
}
|
||||
|
||||
private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) {
|
||||
int len = Math.min(randomBytes.length, csrfBytes.length);
|
||||
Assert.isTrue(randomBytes.length == csrfBytes.length, "arrays must be equal length");
|
||||
int len = csrfBytes.length;
|
||||
byte[] xoredCsrf = new byte[len];
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length);
|
||||
System.arraycopy(csrfBytes, 0, xoredCsrf, 0, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
xoredCsrf[i] ^= randomBytes[i];
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.server.firewall;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A simple implementation of {@link ServerExchangeRejectedHandler} that sends an error
|
||||
* with configurable status code.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public class HttpStatusExchangeRejectedHandler implements ServerExchangeRejectedHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(HttpStatusExchangeRejectedHandler.class);
|
||||
|
||||
private final HttpStatus status;
|
||||
|
||||
/**
|
||||
* Constructs an instance which uses {@code 400} as response code.
|
||||
*/
|
||||
public HttpStatusExchangeRejectedHandler() {
|
||||
this(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance which uses a configurable http code as response.
|
||||
* @param status http status code to use
|
||||
*/
|
||||
public HttpStatusExchangeRejectedHandler(HttpStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange,
|
||||
ServerExchangeRejectedException serverExchangeRejectedException) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
logger.debug(
|
||||
LogMessage.format("Rejecting request due to: %s", serverExchangeRejectedException.getMessage()),
|
||||
serverExchangeRejectedException);
|
||||
exchange.getResponse().setStatusCode(this.status);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.server.firewall;
|
||||
|
||||
/**
|
||||
* Thrown when a {@link org.springframework.web.server.ServerWebExchange} is rejected.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public class ServerExchangeRejectedException extends RuntimeException {
|
||||
|
||||
public ServerExchangeRejectedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.server.firewall;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Handles {@link ServerExchangeRejectedException} thrown by
|
||||
* {@link ServerWebExchangeFirewall}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public interface ServerExchangeRejectedHandler {
|
||||
|
||||
/**
|
||||
* Handles an request rejected failure.
|
||||
* @param exchange the {@link ServerWebExchange} that was rejected
|
||||
* @param serverExchangeRejectedException that caused the invocation
|
||||
*/
|
||||
Mono<Void> handle(ServerWebExchange exchange, ServerExchangeRejectedException serverExchangeRejectedException);
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.server.firewall;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Interface which can be used to reject potentially dangerous requests and/or wrap them
|
||||
* to control their behaviour.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public interface ServerWebExchangeFirewall {
|
||||
|
||||
/**
|
||||
* An implementation of {@link StrictServerWebExchangeFirewall} that does nothing.
|
||||
* This is considered insecure and not recommended.
|
||||
*/
|
||||
ServerWebExchangeFirewall INSECURE_NOOP = (exchange) -> Mono.just(exchange);
|
||||
|
||||
/**
|
||||
* Get a {@link ServerWebExchange} that has firewall rules applied to it.
|
||||
* @param exchange the {@link ServerWebExchange} to apply firewall rules to.
|
||||
* @return the {@link ServerWebExchange} that has firewall rules applied to it.
|
||||
* @throws ServerExchangeRejectedException when a rule is broken.
|
||||
*/
|
||||
Mono<ServerWebExchange> getFirewalledExchange(ServerWebExchange exchange);
|
||||
|
||||
}
|
||||
+791
@@ -0,0 +1,791 @@
|
||||
/*
|
||||
* 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.server.firewall;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchangeDecorator;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A strict implementation of {@link ServerWebExchangeFirewall} that rejects any
|
||||
* suspicious requests with a {@link ServerExchangeRejectedException}.
|
||||
* </p>
|
||||
* <p>
|
||||
* The following rules are applied to the firewall:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Rejects HTTP methods that are not allowed. This specified to block
|
||||
* <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)">HTTP Verb
|
||||
* tampering and XST attacks</a>. See {@link #setAllowedHttpMethods(Collection)}</li>
|
||||
* <li>Rejects URLs that are not normalized to avoid bypassing security constraints. There
|
||||
* is no way to disable this as it is considered extremely risky to disable this
|
||||
* constraint. A few options to allow this behavior is to normalize the request prior to
|
||||
* the firewall or using
|
||||
* {@link org.springframework.security.web.firewall.DefaultHttpFirewall} instead. Please
|
||||
* keep in mind that normalizing the request is fragile and why requests are rejected
|
||||
* rather than normalized.</li>
|
||||
* <li>Rejects URLs that contain characters that are not printable ASCII characters. There
|
||||
* is no way to disable this as it is considered extremely risky to disable this
|
||||
* constraint.</li>
|
||||
* <li>Rejects URLs that contain semicolons. See {@link #setAllowSemicolon(boolean)}</li>
|
||||
* <li>Rejects URLs that contain a URL encoded slash. See
|
||||
* {@link #setAllowUrlEncodedSlash(boolean)}</li>
|
||||
* <li>Rejects URLs that contain a backslash. See {@link #setAllowBackSlash(boolean)}</li>
|
||||
* <li>Rejects URLs that contain a null character. See {@link #setAllowNull(boolean)}</li>
|
||||
* <li>Rejects URLs that contain a URL encoded percent. See
|
||||
* {@link #setAllowUrlEncodedPercent(boolean)}</li>
|
||||
* <li>Rejects hosts that are not allowed. See {@link #setAllowedHostnames(Predicate)}
|
||||
* </li>
|
||||
* <li>Reject headers names that are not allowed. See
|
||||
* {@link #setAllowedHeaderNames(Predicate)}</li>
|
||||
* <li>Reject headers values that are not allowed. See
|
||||
* {@link #setAllowedHeaderValues(Predicate)}</li>
|
||||
* <li>Reject parameter names that are not allowed. See
|
||||
* {@link #setAllowedParameterNames(Predicate)}</li>
|
||||
* <li>Reject parameter values that are not allowed. See
|
||||
* {@link #setAllowedParameterValues(Predicate)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.7.13
|
||||
*/
|
||||
public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewall {
|
||||
|
||||
/**
|
||||
* Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method
|
||||
* should be allowed.
|
||||
*/
|
||||
private static final Set<HttpMethod> ALLOW_ANY_HTTP_METHOD = Collections.emptySet();
|
||||
|
||||
private static final String ENCODED_PERCENT = "%25";
|
||||
|
||||
private static final String PERCENT = "%";
|
||||
|
||||
private static final List<String> FORBIDDEN_ENCODED_PERIOD = Collections
|
||||
.unmodifiableList(Arrays.asList("%2e", "%2E"));
|
||||
|
||||
private static final List<String> FORBIDDEN_SEMICOLON = Collections
|
||||
.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
|
||||
|
||||
private static final List<String> FORBIDDEN_FORWARDSLASH = Collections
|
||||
.unmodifiableList(Arrays.asList("%2f", "%2F"));
|
||||
|
||||
private static final List<String> FORBIDDEN_DOUBLE_FORWARDSLASH = Collections
|
||||
.unmodifiableList(Arrays.asList("//", "%2f%2f", "%2f%2F", "%2F%2f", "%2F%2F"));
|
||||
|
||||
private static final List<String> FORBIDDEN_BACKSLASH = Collections
|
||||
.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
|
||||
|
||||
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<>();
|
||||
|
||||
private Set<HttpMethod> allowedHttpMethods = createDefaultAllowedHttpMethods();
|
||||
|
||||
private Predicate<String> allowedHostnames = (hostname) -> true;
|
||||
|
||||
private static final Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern
|
||||
.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
|
||||
|
||||
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
|
||||
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
|
||||
|
||||
private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
|
||||
|
||||
private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> s == null
|
||||
|| HEADER_VALUE_PATTERN.matcher(s).matches();
|
||||
|
||||
private Predicate<String> allowedHeaderNames = ALLOWED_HEADER_NAMES;
|
||||
|
||||
public static final Predicate<String> ALLOWED_HEADER_NAMES = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedHeaderValues = ALLOWED_HEADER_VALUES;
|
||||
|
||||
public static final Predicate<String> ALLOWED_HEADER_VALUES = HEADER_VALUE_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedParameterNames = ALLOWED_PARAMETER_NAMES;
|
||||
|
||||
public static final Predicate<String> ALLOWED_PARAMETER_NAMES = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
|
||||
|
||||
private Predicate<String> allowedParameterValues = ALLOWED_PARAMETER_VALUES;
|
||||
|
||||
public static final Predicate<String> ALLOWED_PARAMETER_VALUES = (value) -> true;
|
||||
|
||||
public StrictServerWebExchangeFirewall() {
|
||||
urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
|
||||
urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
|
||||
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);
|
||||
}
|
||||
|
||||
public Set<String> getEncodedUrlBlocklist() {
|
||||
return this.encodedUrlBlocklist;
|
||||
}
|
||||
|
||||
public Set<String> getDecodedUrlBlocklist() {
|
||||
return this.decodedUrlBlocklist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerWebExchange> getFirewalledExchange(ServerWebExchange exchange) {
|
||||
return Mono.fromCallable(() -> {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
rejectForbiddenHttpMethod(request);
|
||||
rejectedBlocklistedUrls(request);
|
||||
rejectedUntrustedHosts(request);
|
||||
if (!isNormalized(request)) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the URL was not normalized");
|
||||
}
|
||||
|
||||
exchange.getResponse().beforeCommit(() -> Mono.fromRunnable(() -> {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
|
||||
String headerName = header.getKey();
|
||||
List<String> headerValues = header.getValue();
|
||||
for (String headerValue : headerValues) {
|
||||
validateCrlf(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
}));
|
||||
return new StrictFirewallServerWebExchange(exchange);
|
||||
});
|
||||
}
|
||||
|
||||
private static void validateCrlf(String name, String value) {
|
||||
Assert.isTrue(!hasCrlf(name) && !hasCrlf(value), () -> "Invalid characters (CR/LF) in header " + name);
|
||||
}
|
||||
|
||||
private static boolean hasCrlf(String value) {
|
||||
return value != null && (value.indexOf('\n') != -1 || value.indexOf('\r') != -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if any HTTP method is allowed. If this set to true, then no validation on the
|
||||
* HTTP method will be performed. This can open the application up to
|
||||
* <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)"> HTTP
|
||||
* Verb tampering and XST attacks</a>
|
||||
* @param unsafeAllowAnyHttpMethod if true, disables HTTP method validation, else
|
||||
* resets back to the defaults. Default is false.
|
||||
* @since 5.1
|
||||
* @see #setAllowedHttpMethods(Collection)
|
||||
*/
|
||||
public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
|
||||
this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines which HTTP methods should be allowed. The default is to allow "DELETE",
|
||||
* "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT".
|
||||
* </p>
|
||||
* @param allowedHttpMethods the case-sensitive collection of HTTP methods that are
|
||||
* allowed.
|
||||
* @since 5.1
|
||||
* @see #setUnsafeAllowAnyHttpMethod(boolean)
|
||||
*/
|
||||
public void setAllowedHttpMethods(Collection<HttpMethod> allowedHttpMethods) {
|
||||
Assert.notNull(allowedHttpMethods, "allowedHttpMethods cannot be null");
|
||||
this.allowedHttpMethods = (allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) ? new HashSet<>(allowedHttpMethods)
|
||||
: ALLOW_ANY_HTTP_METHOD;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if semicolon is allowed in the URL (i.e. matrix variables). The default
|
||||
* is to disable this behavior because it is a common way of attempting to perform
|
||||
* <a href="https://www.owasp.org/index.php/Reflected_File_Download">Reflected File
|
||||
* Download Attacks</a>. It is also the source of many exploits which bypass URL based
|
||||
* security.
|
||||
* </p>
|
||||
* <p>
|
||||
* For example, the following CVEs are a subset of the issues related to ambiguities
|
||||
* in the Servlet Specification on how to treat semicolons that led to CVEs:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><a href="https://pivotal.io/security/cve-2016-5007">cve-2016-5007</a></li>
|
||||
* <li><a href="https://pivotal.io/security/cve-2016-9879">cve-2016-9879</a></li>
|
||||
* <li><a href="https://pivotal.io/security/cve-2018-1199">cve-2018-1199</a></li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* If you are wanting to allow semicolons, please reconsider as it is a very common
|
||||
* source of security bypasses. A few common reasons users want semicolons and
|
||||
* alternatives are listed below:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Including the JSESSIONID in the path - You should not include session id (or
|
||||
* any sensitive information) in a URL as it can lead to leaking. Instead use Cookies.
|
||||
* </li>
|
||||
* <li>Matrix Variables - Users wanting to leverage Matrix Variables should consider
|
||||
* using HTTP parameters instead.</li>
|
||||
* </ul>
|
||||
* @param allowSemicolon should semicolons be allowed in the URL. Default is false
|
||||
*/
|
||||
public void setAllowSemicolon(boolean allowSemicolon) {
|
||||
if (allowSemicolon) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_SEMICOLON);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if a slash "/" that is URL encoded "%2F" should be allowed in the path
|
||||
* or not. The default is to not allow this behavior because it is a common way to
|
||||
* bypass URL based security.
|
||||
* </p>
|
||||
* <p>
|
||||
* For example, due to ambiguities in the servlet specification, the value is not
|
||||
* parsed consistently which results in different values in {@code HttpServletRequest}
|
||||
* path related values which allow bypassing certain security constraints.
|
||||
* </p>
|
||||
* @param allowUrlEncodedSlash should a slash "/" that is URL encoded "%2F" be allowed
|
||||
* in the path or not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
|
||||
if (allowUrlEncodedSlash) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if double slash "//" that is URL encoded "%2F%2F" should be allowed in
|
||||
* the path or not. The default is to not allow.
|
||||
* </p>
|
||||
* @param allowUrlEncodedDoubleSlash should a slash "//" that is URL encoded "%2F%2F"
|
||||
* be allowed in the path or not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedDoubleSlash(boolean allowUrlEncodedDoubleSlash) {
|
||||
if (allowUrlEncodedDoubleSlash) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if a period "." that is URL encoded "%2E" should be allowed in the path
|
||||
* or not. The default is to not allow this behavior because it is a frequent source
|
||||
* of security exploits.
|
||||
* </p>
|
||||
* <p>
|
||||
* For example, due to ambiguities in the servlet specification a URL encoded period
|
||||
* might lead to bypassing security constraints through a directory traversal attack.
|
||||
* This is because the path is not parsed consistently which results in different
|
||||
* values in {@code HttpServletRequest} path related values which allow bypassing
|
||||
* certain security constraints.
|
||||
* </p>
|
||||
* @param allowUrlEncodedPeriod should a period "." that is URL encoded "%2E" be
|
||||
* allowed in the path or not. Default is false.
|
||||
*/
|
||||
public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) {
|
||||
if (allowUrlEncodedPeriod) {
|
||||
this.encodedUrlBlocklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
|
||||
}
|
||||
else {
|
||||
this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if a backslash "\" or a URL encoded backslash "%5C" should be allowed in
|
||||
* the path or not. The default is not to allow this behavior because it is a frequent
|
||||
* source of security exploits.
|
||||
* </p>
|
||||
* <p>
|
||||
* For example, due to ambiguities in the servlet specification a URL encoded period
|
||||
* might lead to bypassing security constraints through a directory traversal attack.
|
||||
* This is because the path is not parsed consistently which results in different
|
||||
* values in {@code HttpServletRequest} path related values which allow bypassing
|
||||
* certain security constraints.
|
||||
* </p>
|
||||
* @param allowBackSlash a backslash "\" or a URL encoded backslash "%5C" be allowed
|
||||
* in the path or not. Default is false
|
||||
*/
|
||||
public void setAllowBackSlash(boolean allowBackSlash) {
|
||||
if (allowBackSlash) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_BACKSLASH);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_BACKSLASH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if a null "\0" or a URL encoded nul "%00" should be allowed in the path
|
||||
* or not. The default is not to allow this behavior because it is a frequent source
|
||||
* of security exploits.
|
||||
* </p>
|
||||
* @param allowNull a null "\0" or a URL encoded null "%00" be allowed in the path or
|
||||
* not. Default is false
|
||||
* @since 5.4
|
||||
*/
|
||||
public void setAllowNull(boolean allowNull) {
|
||||
if (allowNull) {
|
||||
urlBlocklistsRemoveAll(FORBIDDEN_NULL);
|
||||
}
|
||||
else {
|
||||
urlBlocklistsAddAll(FORBIDDEN_NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if a percent "%" that is URL encoded "%25" should be allowed in the path
|
||||
* or not. The default is not to allow this behavior because it is a frequent source
|
||||
* of security exploits.
|
||||
* </p>
|
||||
* <p>
|
||||
* For example, this can lead to exploits that involve double URL encoding that lead
|
||||
* to bypassing security constraints.
|
||||
* </p>
|
||||
* @param allowUrlEncodedPercent if a percent "%" that is URL encoded "%25" should be
|
||||
* allowed in the path or not. Default is false
|
||||
*/
|
||||
public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
|
||||
if (allowUrlEncodedPercent) {
|
||||
this.encodedUrlBlocklist.remove(ENCODED_PERCENT);
|
||||
this.decodedUrlBlocklist.remove(PERCENT);
|
||||
}
|
||||
else {
|
||||
this.encodedUrlBlocklist.add(ENCODED_PERCENT);
|
||||
this.decodedUrlBlocklist.add(PERCENT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* names that contain ISO control characters and characters that are not defined.
|
||||
* </p>
|
||||
* @param allowedHeaderNames the predicate for testing header names
|
||||
* @since 5.4
|
||||
* @see Character#isISOControl(int)
|
||||
* @see Character#isDefined(int)
|
||||
*/
|
||||
public void setAllowedHeaderNames(Predicate<String> allowedHeaderNames) {
|
||||
Assert.notNull(allowedHeaderNames, "allowedHeaderNames cannot be null");
|
||||
this.allowedHeaderNames = allowedHeaderNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines which header values should be allowed. The default is to reject header
|
||||
* values that contain ISO control characters and characters that are not defined.
|
||||
* </p>
|
||||
* @param allowedHeaderValues the predicate for testing hostnames
|
||||
* @since 5.4
|
||||
* @see Character#isISOControl(int)
|
||||
* @see Character#isDefined(int)
|
||||
*/
|
||||
public void setAllowedHeaderValues(Predicate<String> allowedHeaderValues) {
|
||||
Assert.notNull(allowedHeaderValues, "allowedHeaderValues cannot be null");
|
||||
this.allowedHeaderValues = allowedHeaderValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which parameter names should be allowed. The default is to reject header
|
||||
* names that contain ISO control characters and characters that are not defined.
|
||||
* @param allowedParameterNames the predicate for testing parameter names
|
||||
* @since 5.4
|
||||
* @see Character#isISOControl(int)
|
||||
* @see Character#isDefined(int)
|
||||
*/
|
||||
public void setAllowedParameterNames(Predicate<String> allowedParameterNames) {
|
||||
Assert.notNull(allowedParameterNames, "allowedParameterNames cannot be null");
|
||||
this.allowedParameterNames = allowedParameterNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines which parameter values should be allowed. The default is to allow any
|
||||
* parameter value.
|
||||
* </p>
|
||||
* @param allowedParameterValues the predicate for testing parameter values
|
||||
* @since 5.4
|
||||
*/
|
||||
public void setAllowedParameterValues(Predicate<String> allowedParameterValues) {
|
||||
Assert.notNull(allowedParameterValues, "allowedParameterValues cannot be null");
|
||||
this.allowedParameterValues = allowedParameterValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines which hostnames should be allowed. The default is to allow any hostname.
|
||||
* </p>
|
||||
* @param allowedHostnames the predicate for testing hostnames
|
||||
* @since 5.2
|
||||
*/
|
||||
public void setAllowedHostnames(Predicate<String> allowedHostnames) {
|
||||
Assert.notNull(allowedHostnames, "allowedHostnames cannot be null");
|
||||
this.allowedHostnames = allowedHostnames;
|
||||
}
|
||||
|
||||
private void urlBlocklistsAddAll(Collection<String> values) {
|
||||
this.encodedUrlBlocklist.addAll(values);
|
||||
this.decodedUrlBlocklist.addAll(values);
|
||||
}
|
||||
|
||||
private void urlBlocklistsRemoveAll(Collection<String> values) {
|
||||
this.encodedUrlBlocklist.removeAll(values);
|
||||
this.decodedUrlBlocklist.removeAll(values);
|
||||
}
|
||||
|
||||
private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) {
|
||||
if (!containsOnlyPrintableAsciiCharacters(toCheck)) {
|
||||
throw new ServerExchangeRejectedException(String
|
||||
.format("The %s was rejected because it can only contain printable ASCII characters.", propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectForbiddenHttpMethod(ServerHttpRequest request) {
|
||||
if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
|
||||
return;
|
||||
}
|
||||
if (!this.allowedHttpMethods.contains(request.getMethod())) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the HTTP method \"" + request.getMethod()
|
||||
+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectedBlocklistedUrls(ServerHttpRequest request) {
|
||||
for (String forbidden : this.encodedUrlBlocklist) {
|
||||
if (encodedUrlContains(request, forbidden)) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the URL contained a potentially malicious String \""
|
||||
+ forbidden + "\"");
|
||||
}
|
||||
}
|
||||
for (String forbidden : this.decodedUrlBlocklist) {
|
||||
if (decodedUrlContains(request, forbidden)) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the URL contained a potentially malicious String \""
|
||||
+ forbidden + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectedUntrustedHosts(ServerHttpRequest request) {
|
||||
String hostName = request.getURI().getHost();
|
||||
if (hostName != null && !this.allowedHostnames.test(hostName)) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the domain " + hostName + " is untrusted.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<HttpMethod> createDefaultAllowedHttpMethods() {
|
||||
Set<HttpMethod> result = new HashSet<>();
|
||||
result.add(HttpMethod.DELETE);
|
||||
result.add(HttpMethod.GET);
|
||||
result.add(HttpMethod.HEAD);
|
||||
result.add(HttpMethod.OPTIONS);
|
||||
result.add(HttpMethod.PATCH);
|
||||
result.add(HttpMethod.POST);
|
||||
result.add(HttpMethod.PUT);
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isNormalized(ServerHttpRequest request) {
|
||||
if (!isNormalized(request.getPath().value())) {
|
||||
return false;
|
||||
}
|
||||
if (!isNormalized(request.getURI().getRawPath())) {
|
||||
return false;
|
||||
}
|
||||
if (!isNormalized(request.getURI().getPath())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void validateAllowedHeaderName(String headerNames) {
|
||||
if (!StrictServerWebExchangeFirewall.this.allowedHeaderNames.test(headerNames)) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the header name \"" + headerNames + "\" is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAllowedHeaderValue(Object key, String value) {
|
||||
if (!StrictServerWebExchangeFirewall.this.allowedHeaderValues.test(value)) {
|
||||
throw new ServerExchangeRejectedException("The request was rejected because the header: \"" + key
|
||||
+ " \" has a value \"" + value + "\" that is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAllowedParameterName(String name) {
|
||||
if (!StrictServerWebExchangeFirewall.this.allowedParameterNames.test(name)) {
|
||||
throw new ServerExchangeRejectedException(
|
||||
"The request was rejected because the parameter name \"" + name + "\" is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAllowedParameterValue(String name, String value) {
|
||||
if (!StrictServerWebExchangeFirewall.this.allowedParameterValues.test(value)) {
|
||||
throw new ServerExchangeRejectedException("The request was rejected because the parameter: \"" + name
|
||||
+ " \" has a value \"" + value + "\" that is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean encodedUrlContains(ServerHttpRequest request, String value) {
|
||||
if (valueContains(request.getPath().value(), value)) {
|
||||
return true;
|
||||
}
|
||||
return valueContains(request.getURI().getRawPath(), value);
|
||||
}
|
||||
|
||||
private static boolean decodedUrlContains(ServerHttpRequest request, String value) {
|
||||
return valueContains(request.getURI().getPath(), value);
|
||||
}
|
||||
|
||||
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);
|
||||
if (ch < '\u0020' || ch > '\u007e') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean valueContains(String value, String contains) {
|
||||
return value != null && value.contains(contains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a path is normalized (doesn't contain path traversal sequences like
|
||||
* "./", "/../" or "/.")
|
||||
* @param path the path to test
|
||||
* @return true if the path doesn't contain any path-traversal character sequences.
|
||||
*/
|
||||
private static boolean isNormalized(String path) {
|
||||
if (path == null) {
|
||||
return true;
|
||||
}
|
||||
for (int i = path.length(); i > 0;) {
|
||||
int slashIndex = path.lastIndexOf('/', i - 1);
|
||||
int gap = i - slashIndex;
|
||||
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
|
||||
return false; // ".", "/./" or "/."
|
||||
}
|
||||
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
|
||||
return false;
|
||||
}
|
||||
i = slashIndex;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private final class StrictFirewallServerWebExchange extends ServerWebExchangeDecorator {
|
||||
|
||||
private StrictFirewallServerWebExchange(ServerWebExchange delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerHttpRequest getRequest() {
|
||||
return new StrictFirewallHttpRequest(super.getRequest());
|
||||
}
|
||||
|
||||
private final class StrictFirewallHttpRequest extends ServerHttpRequestDecorator {
|
||||
|
||||
private StrictFirewallHttpRequest(ServerHttpRequest delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return new StrictFirewallHttpHeaders(super.getHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiValueMap<String, String> getQueryParams() {
|
||||
MultiValueMap<String, String> queryParams = super.getQueryParams();
|
||||
for (Map.Entry<String, List<String>> paramEntry : queryParams.entrySet()) {
|
||||
String paramName = paramEntry.getKey();
|
||||
validateAllowedParameterName(paramName);
|
||||
for (String paramValue : paramEntry.getValue()) {
|
||||
validateAllowedParameterValue(paramName, paramValue);
|
||||
}
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
private final class StrictFirewallHttpHeaders extends HttpHeaders {
|
||||
|
||||
private StrictFirewallHttpHeaders(HttpHeaders delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirst(String headerName) {
|
||||
validateAllowedHeaderName(headerName);
|
||||
String headerValue = super.getFirst(headerName);
|
||||
validateAllowedHeaderValue(headerName, headerValue);
|
||||
return headerValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> get(Object key) {
|
||||
if (key instanceof String) {
|
||||
String headerName = (String) key;
|
||||
validateAllowedHeaderName(headerName);
|
||||
}
|
||||
List<String> headerValues = super.get(key);
|
||||
if (headerValues == null) {
|
||||
return headerValues;
|
||||
}
|
||||
for (String headerValue : headerValues) {
|
||||
validateAllowedHeaderValue(key, headerValue);
|
||||
}
|
||||
return headerValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
Set<String> headerNames = super.keySet();
|
||||
for (String headerName : headerNames) {
|
||||
validateAllowedHeaderName(headerName);
|
||||
}
|
||||
return headerNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.security.web.util;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -49,7 +50,7 @@ public final class UrlUtils {
|
||||
*/
|
||||
public static String buildFullRequestUrl(String scheme, String serverName, int serverPort, String requestURI,
|
||||
String queryString) {
|
||||
scheme = scheme.toLowerCase();
|
||||
scheme = scheme.toLowerCase(Locale.ENGLISH);
|
||||
StringBuilder url = new StringBuilder();
|
||||
url.append(scheme).append("://").append(serverName);
|
||||
// Only add port if not default
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -304,7 +305,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
|
||||
|
||||
private SubpathMatcher(String subpath, boolean caseSensitive) {
|
||||
Assert.isTrue(!subpath.contains("*"), "subpath cannot contain \"*\"");
|
||||
this.subpath = caseSensitive ? subpath : subpath.toLowerCase();
|
||||
this.subpath = caseSensitive ? subpath : subpath.toLowerCase(Locale.ROOT);
|
||||
this.length = subpath.length();
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
@@ -312,7 +313,7 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
|
||||
@Override
|
||||
public boolean matches(String path) {
|
||||
if (!this.caseSensitive) {
|
||||
path = path.toLowerCase();
|
||||
path = path.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return path.startsWith(this.subpath) && (path.length() == this.length || path.charAt(this.length) == '/');
|
||||
}
|
||||
|
||||
+56
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -44,6 +44,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*/
|
||||
public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -203,6 +206,58 @@ public class XorCsrfTokenRequestAttributeHandlerTests {
|
||||
assertThat(tokenValue).isEqualTo(this.token.getToken());
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.request.setParameter(this.token.getParameterName(), actualToken);
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE);
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.request.setParameter(this.token.getParameterName(), "");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.request, this.token);
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
+11
-1
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -213,4 +213,14 @@ public class CookieRequestCacheTests {
|
||||
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
|
||||
}
|
||||
|
||||
// gh-15905
|
||||
@Test
|
||||
public void illegalCookieValueReturnNull() {
|
||||
CookieRequestCache cookieRequestCache = new CookieRequestCache();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, "123^456"));
|
||||
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
|
||||
assertThat(savedRequest).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+81
@@ -23,6 +23,9 @@ import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.web.server.firewall.ServerExchangeRejectedException;
|
||||
import org.springframework.security.web.server.firewall.ServerExchangeRejectedHandler;
|
||||
import org.springframework.security.web.server.firewall.ServerWebExchangeFirewall;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
@@ -30,6 +33,12 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @since 5.0
|
||||
@@ -52,6 +61,78 @@ public class WebFilterChainProxyTests {
|
||||
.isNotFound();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenFirewallThenBadRequest() {
|
||||
List<WebFilter> filters = Arrays.asList(new Http200WebFilter());
|
||||
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
|
||||
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
|
||||
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
|
||||
WebTestClient.bindToController(new Object())
|
||||
.webFilter(filter)
|
||||
.build()
|
||||
.get()
|
||||
.uri("/invalid;/")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenCustomFirewallThenInvoked() {
|
||||
List<WebFilter> filters = Arrays.asList(new Http200WebFilter());
|
||||
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
|
||||
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
|
||||
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
|
||||
ServerExchangeRejectedHandler handler = mock(ServerExchangeRejectedHandler.class);
|
||||
ServerWebExchangeFirewall firewall = mock(ServerWebExchangeFirewall.class);
|
||||
filter.setFirewall(firewall);
|
||||
filter.setExchangeRejectedHandler(handler);
|
||||
WebTestClient.bindToController(new Object()).webFilter(filter).build().get().exchange();
|
||||
verify(firewall).getFirewalledExchange(any());
|
||||
verifyNoInteractions(handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenCustomExchangeRejectedHandlerThenInvoked() {
|
||||
List<WebFilter> filters = Arrays.asList(new Http200WebFilter());
|
||||
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
|
||||
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
|
||||
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
|
||||
ServerExchangeRejectedHandler handler = mock(ServerExchangeRejectedHandler.class);
|
||||
ServerWebExchangeFirewall firewall = mock(ServerWebExchangeFirewall.class);
|
||||
given(firewall.getFirewalledExchange(any()))
|
||||
.willReturn(Mono.error(new ServerExchangeRejectedException("Oops")));
|
||||
filter.setFirewall(firewall);
|
||||
filter.setExchangeRejectedHandler(handler);
|
||||
WebTestClient.bindToController(new Object()).webFilter(filter).build().get().exchange();
|
||||
verify(firewall).getFirewalledExchange(any());
|
||||
verify(handler).handle(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doFilterWhenDelayedServerExchangeRejectedException() {
|
||||
List<WebFilter> filters = Arrays.asList(new WebFilter() {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
// simulate a delayed error (e.g. reading parameters)
|
||||
return Mono.error(new ServerExchangeRejectedException("Ooops"));
|
||||
}
|
||||
});
|
||||
ServerWebExchangeMatcher match = (exchange) -> MatchResult.match();
|
||||
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(match, filters);
|
||||
WebFilterChainProxy filter = new WebFilterChainProxy(chain);
|
||||
ServerExchangeRejectedHandler handler = mock(ServerExchangeRejectedHandler.class);
|
||||
filter.setExchangeRejectedHandler(handler);
|
||||
// @formatter:off
|
||||
WebTestClient.bindToController(new Object())
|
||||
.webFilter(filter)
|
||||
.build()
|
||||
.get()
|
||||
.exchange();
|
||||
// @formatter:on
|
||||
verify(handler).handle(any(), any());
|
||||
}
|
||||
|
||||
static class Http200WebFilter implements WebFilter {
|
||||
|
||||
@Override
|
||||
|
||||
+76
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -46,6 +46,9 @@ import static org.mockito.Mockito.verify;
|
||||
*/
|
||||
public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 3 padded bytes.
|
||||
*/
|
||||
private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
|
||||
|
||||
private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES);
|
||||
@@ -188,6 +191,78 @@ public class XorServerCsrfTokenRequestAttributeHandlerTests {
|
||||
StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsShorterThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 2 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenCsrfBytesIsLongerThanRandomBytesThenReturnsNull() {
|
||||
/*
|
||||
* Token format: 3 random pad bytes + 4 padded bytes.
|
||||
*/
|
||||
byte[] actualBytes = { 1, 1, 1, 96, 99, 98, 97 };
|
||||
String actualToken = Base64.getEncoder().encodeToString(actualBytes);
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), actualToken))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsShorterThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenTokenBytesIsLongerThanActualBytesThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE))
|
||||
.build();
|
||||
CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "abcde");
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, csrfToken).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
// gh-13310, gh-15184
|
||||
@Test
|
||||
public void resolveCsrfTokenValueWhenActualBytesIsEmptyThenReturnsNull() {
|
||||
this.exchange = MockServerWebExchange
|
||||
.builder(MockServerHttpRequest.post("/")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.header(this.token.getHeaderName(), ""))
|
||||
.build();
|
||||
String tokenValue = this.handler.resolveCsrfTokenValue(this.exchange, this.token).block();
|
||||
assertThat(tokenValue).isNull();
|
||||
}
|
||||
|
||||
private static Answer<Void> fillByteArray() {
|
||||
return (invocation) -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
/*
|
||||
* 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.server.firewall;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link StrictServerWebExchangeFirewall}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 5.7.13
|
||||
*/
|
||||
class StrictServerWebExchangeFirewallTests {
|
||||
|
||||
public String[] unnormalizedPaths = { "http://exploit.example/..", "http://exploit.example/./path/",
|
||||
"http://exploit.example/path/path/.", "http://exploit.example/path/path//.",
|
||||
"http://exploit.example/./path/../path//.", "http://exploit.example/./path",
|
||||
"http://exploit.example/.//path", "http://exploit.example/.", "http://exploit.example//path",
|
||||
"http://exploit.example//path/path", "http://exploit.example//path//path",
|
||||
"http://exploit.example/path//path" };
|
||||
|
||||
private StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
|
||||
private MockServerHttpRequest.BaseBuilder<?> request = get("/");
|
||||
|
||||
@Test
|
||||
void cookieWhenHasNewLineThenThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ResponseCookie.from("test", "Something\nhere").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieWhenHasLineFeedThenThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ResponseCookie.from("test", "Something\rhere").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseHeadersWhenValueHasNewLineThenThrowsException() {
|
||||
this.request = MockServerHttpRequest.get("/");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
exchange.getResponse().getHeaders().set("FOO", "new\nline");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseHeadersWhenValueHasLineFeedThenThrowsException() {
|
||||
this.request = MockServerHttpRequest.get("/");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
exchange.getResponse().getHeaders().set("FOO", "line\rfeed");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseHeadersWhenNameHasNewLineThenThrowsException() {
|
||||
this.request = MockServerHttpRequest.get("/");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
exchange.getResponse().getHeaders().set("new\nline", "FOO");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseHeadersWhenNameHasLineFeedThenThrowsException() {
|
||||
this.request = MockServerHttpRequest.get("/");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
exchange.getResponse().getHeaders().set("line\rfeed", "FOO");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> exchange.getResponse().setComplete().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenInvalidMethodThenThrowsServerExchangeRejectedException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MockServerHttpRequest.method(HttpMethod.valueOf("INVALID"), "/"));
|
||||
}
|
||||
|
||||
private ServerWebExchange getFirewalledExchange() {
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(this.request.build());
|
||||
return this.firewall.getFirewalledExchange(exchange).block();
|
||||
}
|
||||
|
||||
private MockServerHttpRequest.BodyBuilder get(String uri) {
|
||||
URI url = URI.create(uri);
|
||||
return MockServerHttpRequest.method(HttpMethod.GET, url);
|
||||
}
|
||||
|
||||
// blocks XST attacks
|
||||
@Test
|
||||
void getFirewalledExchangeWhenTraceMethodThenThrowsServerExchangeRejectedException() {
|
||||
this.request = MockServerHttpRequest.method(HttpMethod.TRACE, "/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
// blocks XST attack if request is forwarded to a Microsoft IIS web server
|
||||
void getFirewalledExchangeWhenTrackMethodThenThrowsServerExchangeRejectedException() {
|
||||
this.request = MockServerHttpRequest.method(HttpMethod.TRACE, "/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
// HTTP methods are case sensitive
|
||||
void getFirewalledExchangeWhenLowercaseGetThenThrowsServerExchangeRejectedException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> HttpMethod.valueOf("get"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowedThenNoException() {
|
||||
List<String> allowedMethods = Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT");
|
||||
for (String allowedMethod : allowedMethods) {
|
||||
this.request = MockServerHttpRequest.method(HttpMethod.valueOf(allowedMethod), "/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenInvalidMethodAndAnyMethodThenNoException() {
|
||||
this.firewall.setUnsafeAllowAnyHttpMethod(true);
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> MockServerHttpRequest.method(HttpMethod.valueOf("INVALID"), "/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenURINotNormalizedThenThrowsServerExchangeRejectedException() {
|
||||
for (String path : this.unnormalizedPaths) {
|
||||
this.request = get(path);
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class)
|
||||
.describedAs("The path '" + path + "' is not normalized")
|
||||
.isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenSemicolonInRequestUriThenThrowsServerExchangeRejectedException() {
|
||||
this.request = get("/path;/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenEncodedSemicolonInRequestUriThenThrowsServerExchangeRejectedException() {
|
||||
this.request = get("/path%3B/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenLowercaseEncodedSemicolonInRequestUriThenThrowsServerExchangeRejectedException() {
|
||||
this.request = MockServerHttpRequest.method(HttpMethod.GET, URI.create("/path%3b/"));
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenSemicolonInRequestUriAndAllowSemicolonThenNoException() {
|
||||
this.firewall.setAllowSemicolon(true);
|
||||
this.request = get("/path;/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() {
|
||||
this.firewall.setAllowSemicolon(true);
|
||||
this.request = get("/path%3B/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenLowercaseEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() {
|
||||
this.firewall.setAllowSemicolon(true);
|
||||
this.request = get("/path%3b/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenLowercaseEncodedPeriodInThenThrowsServerExchangeRejectedException() {
|
||||
this.request = get("/%2e/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsLowerboundAsciiThenNoException() {
|
||||
this.request = get("/%20");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsUpperboundAsciiThenNoException() {
|
||||
this.request = get("/~");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenJapaneseCharacterThenNoException() {
|
||||
// FIXME: .method(HttpMethod.GET to .get and similar methods
|
||||
this.request = get("/\u3042");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsEncodedNullThenException() {
|
||||
this.request = get("/something%00/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsLowercaseEncodedLineFeedThenException() {
|
||||
this.request = get("/something%0a/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsUppercaseEncodedLineFeedThenException() {
|
||||
this.request = get("/something%0A/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsLowercaseEncodedCarriageReturnThenException() {
|
||||
this.request = get("/something%0d/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsUppercaseEncodedCarriageReturnThenException() {
|
||||
this.request = get("/something%0D/");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsLowercaseEncodedLineFeedAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedLineFeed(true);
|
||||
this.request = get("/something%0a/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsUppercaseEncodedLineFeedAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedLineFeed(true);
|
||||
this.request = get("/something%0A/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsLowercaseEncodedCarriageReturnAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedCarriageReturn(true);
|
||||
this.request = get("/something%0d/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenContainsUppercaseEncodedCarriageReturnAndAllowedThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedCarriageReturn(true);
|
||||
this.request = get("/something%0D/");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* will strip the ; content from requestURI before the path is URL decoded.
|
||||
*/
|
||||
@Test
|
||||
void getFirewalledExchangeWhenLowercaseEncodedPathThenException() {
|
||||
this.request = get("/context-root/a/b;%2f1/c");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenUppercaseEncodedPathThenException() {
|
||||
this.request = get("/context-root/a/b;%2F1/c");
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowUrlEncodedSlashAndLowercaseEncodedPathThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.firewall.setAllowSemicolon(true);
|
||||
this.request = get("/context-root/a/b;%2f1/c");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowUrlEncodedSlashAndUppercaseEncodedPathThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.firewall.setAllowSemicolon(true);
|
||||
this.request = get("/context-root/a/b;%2F1/c");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowUrlLowerCaseEncodedDoubleSlashThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
this.request = get("/context-root/a/b%2f%2fc");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowUrlUpperCaseEncodedDoubleSlashThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
this.request = get("/context-root/a/b%2F%2Fc");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowUrlLowerCaseAndUpperCaseEncodedDoubleSlashThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
this.request = get("/context-root/a/b%2f%2Fc");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenAllowUrlUpperCaseAndLowerCaseEncodedDoubleSlashThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.firewall.setAllowUrlEncodedDoubleSlash(true);
|
||||
this.request = get("/context-root/a/b%2F%2fc");
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenRemoveFromUpperCaseEncodedUrlBlocklistThenNoException() {
|
||||
this.firewall.setAllowUrlEncodedSlash(true);
|
||||
this.request = get("/context-root/a/b%2Fc");
|
||||
this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F%2F"));
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenRemoveFromDecodedUrlBlocklistThenNoException() {
|
||||
this.request = get("/a/b%2F%2Fc");
|
||||
this.firewall.getDecodedUrlBlocklist().removeAll(Arrays.asList("//"));
|
||||
this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F%2F"));
|
||||
this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F"));
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenTrustedDomainThenNoException() {
|
||||
this.request.header("Host", "example.org");
|
||||
this.firewall.setAllowedHostnames((hostname) -> hostname.equals("example.org"));
|
||||
getFirewalledExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenUntrustedDomainThenException() {
|
||||
this.request = get("https://example.org");
|
||||
this.firewall.setAllowedHostnames((hostname) -> hostname.equals("myexample.org"));
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> getFirewalledExchange());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenNotAllowedHeaderNameThenException() {
|
||||
this.firewall.setAllowedHeaderNames((name) -> !name.equals("bad name"));
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("bad name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenHeaderNameNotAllowedWithAugmentedHeaderNamesThenException() {
|
||||
this.firewall.setAllowedHeaderNames(
|
||||
StrictServerWebExchangeFirewall.ALLOWED_HEADER_NAMES.and((name) -> !name.equals("bad name")));
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.getFirst("bad name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenNotAllowedHeaderValueThenException() {
|
||||
this.request.header("good name", "bad value");
|
||||
this.firewall.setAllowedHeaderValues((value) -> !value.equals("bad value"));
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("good name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeWhenHeaderValueNotAllowedWithAugmentedHeaderValuesThenException() {
|
||||
this.request.header("good name", "bad value");
|
||||
this.firewall.setAllowedHeaderValues(
|
||||
StrictServerWebExchangeFirewall.ALLOWED_HEADER_VALUES.and((value) -> !value.equals("bad value")));
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("good name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetDateHeaderWhenControlCharacterInHeaderNameThenException() {
|
||||
this.request.header("Bad\0Name", "some value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Bad\0Name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenUndefinedCharacterInHeaderNameThenException() {
|
||||
this.request.header("Bad\uFFFEName", "some value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Bad\uFFFEName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeadersWhenControlCharacterInHeaderNameThenException() {
|
||||
this.request.header("Bad\0Name", "some value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Bad\0Name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderNamesWhenControlCharacterInHeaderNameThenException() {
|
||||
this.request.header("Bad\0Name", "some value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class)
|
||||
.isThrownBy(() -> headers.keySet().iterator().next());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenControlCharacterInHeaderValueThenException() {
|
||||
this.request.header("Something", "bad\0value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenHorizontalTabInHeaderValueThenNoException() {
|
||||
this.request.header("Something", "tab\tvalue");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThat(headers.getFirst("Something")).isEqualTo("tab\tvalue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
|
||||
this.request.header("Something", "bad\uFFFEvalue");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class)
|
||||
.isThrownBy(() -> headers.getFirst("Something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeadersWhenControlCharacterInHeaderValueThenException() {
|
||||
this.request.header("Something", "bad\0value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> headers.get("Something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetParameterWhenControlCharacterInParameterNameThenException() {
|
||||
this.request.queryParam("Bad\0Name", "some value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(request::getQueryParams);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetParameterValuesWhenNotAllowedInParameterValueThenException() {
|
||||
this.firewall.setAllowedParameterValues((value) -> !value.equals("bad value"));
|
||||
this.request.queryParam("Something", "bad value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> request.getQueryParams());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirewalledExchangeGetParameterValuesWhenNotAllowedInParameterNameThenException() {
|
||||
this.firewall.setAllowedParameterNames((value) -> !value.equals("bad name"));
|
||||
this.request.queryParam("bad name", "good value");
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
assertThatExceptionOfType(ServerExchangeRejectedException.class).isThrownBy(() -> request.getQueryParams());
|
||||
}
|
||||
|
||||
// gh-9598
|
||||
@Test
|
||||
void getFirewalledExchangeGetHeaderWhenNameIsNullThenNull() {
|
||||
ServerWebExchange exchange = getFirewalledExchange();
|
||||
assertThat(exchange.getRequest().getHeaders().get(null)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user