1
0
mirror of synced 2026-07-09 12:50:19 +00:00

Compare commits

...

51 Commits

Author SHA1 Message Date
Rob Winch 8806e0176c rm .github/workflows for unsupported branch 2025-05-02 12:35:20 -05:00
Rob Winch 655c638c97 rm merge-dependabot-pr.yml from Unsupported Branch 2025-04-25 13:12:39 -05:00
Josh Cummings eb313ec901 Link to Messaging SpEL Migration Details
Issue gh-12650
2024-12-09 15:26:54 -07:00
Josh Cummings 8b9fe13c88 Document Messaging SpEL Migration
Issue gh-12650
2024-12-09 15:25:33 -07:00
dependabot[bot] 16272f634c Bump @antora/collector-extension from 1.0.0 to 1.0.1 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 16:48:56 -08:00
dependabot[bot] 0b9887505e Bump antora from 3.2.0-alpha.6 to 3.2.0-alpha.8 in /docs
Bumps [antora](https://gitlab.com/antora/antora) from 3.2.0-alpha.6 to 3.2.0-alpha.8.
- [Changelog](https://gitlab.com/antora/antora/blob/main/CHANGELOG.adoc)
- [Commits](https://gitlab.com/antora/antora/compare/v3.2.0-alpha.6...v3.2.0-alpha.8)

---
updated-dependencies:
- dependency-name: antora
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 16:48:02 -08:00
dependabot[bot] a6c3d123ed Bump @antora/collector-extension from 1.0.0-rc.1 to 1.0.0 in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 17:08:47 -08:00
dependabot[bot] 1dbaa08cd4 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-24 16:48:27 -08:00
github-actions[bot] f38129b5a0 Update Antora Spring UI to v0.4.18 2024-11-18 16:18:27 -06:00
Steve Riesenberg e44fc3817a Merge branch '5.7.x' into 5.8.x 2024-11-18 16:04:13 -06:00
Josh Cummings 85248083c0 Add Dark Mode CSS Style
Closes gh-14834
2024-11-18 12:10:48 -07:00
github-actions[bot] c62168ca5b Next development version 2024-11-18 17:12:34 +00:00
github-actions[bot] d0b2b33dce Release 5.7.14 2024-11-18 16:36:56 +00:00
github-actions[bot] 7881660ca0 Next development version 2024-11-18 16:01:43 +00:00
github-actions[bot] 506e5b7f11 Release 5.8.16 2024-11-18 15:23:30 +00:00
Joe Grandja a7bf8f7cc6 Require Locale argument for toLower/toUpperCase usage 2024-11-18 04:03:07 -05:00
dependabot[bot] 451fbf0227 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-17 16:59:48 -08:00
Joe Grandja 0eaffb37e7 Require Locale argument for toLower/toUpperCase usage 2024-11-15 11:01:23 -05:00
dependabot[bot] d985d044bc Bump org.springframework.ldap:spring-ldap-core from 2.4.2 to 2.4.4
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 2.4.2 to 2.4.4.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/2.4.2...2.4.4)

---
updated-dependencies:
- dependency-name: org.springframework.ldap:spring-ldap-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-14 19:40:38 -08:00
Rob Winch c24b5ebe98 Support ServerExchangeRejectedHandler @Bean
Closes gh-15975
2024-11-11 14:42:19 -06:00
Rob Winch fd900c288e checkExpectedBranchVersion trim version 2024-11-11 14:42:18 -06:00
Rob Winch e86d88d0cf Support ServerExchangeRejectedHandler @Bean
Closes gh-15975
2024-11-08 14:48:40 -06:00
dependabot[bot] f28c26fd54 Bump org.hsqldb:hsqldb from 2.7.3 to 2.7.4
Bumps org.hsqldb:hsqldb from 2.7.3 to 2.7.4.

---
updated-dependencies:
- dependency-name: org.hsqldb:hsqldb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 19:47:57 -08:00
dependabot[bot] 421430330a Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-03 16:34:33 -08:00
Rob Winch 5c2106b22e Format 2024-10-25 12:24:54 -05:00
Rob Winch 48241deba3 Merge branch '5.7.x' into 5.8.x
Support ServerWebExchangeFirewall @Bean

Closes gh-15977
2024-10-25 12:11:57 -05:00
Rob Winch 9cc11be9f3 Merge branch '5.7.x' (early part) into 5.8.x 2024-10-25 12:11:51 -05:00
DingHao 1399a82ea9 Return Null Request When Cookie Is Malformed
Closes gh-15905
2024-10-24 10:55:36 -07:00
github-actions[bot] ec33e40748 Update Antora Spring UI to v0.4.17 2024-10-24 11:12:30 -05:00
Rob Winch e48d6b039b Support ServerWebExchangeFirewall @Bean
Closes gh-15974
2024-10-22 18:25:28 -05:00
github-actions[bot] dd3c6892e9 Next development version 2024-10-21 17:58:56 +00:00
github-actions[bot] f9f533499b Next development version 2024-10-21 17:51:39 +00:00
github-actions[bot] c552366a78 Release 5.7.13 2024-10-21 17:23:29 +00:00
github-actions[bot] 72a208e02c Release 5.8.15 2024-10-21 17:16:06 +00:00
Steve Riesenberg 7215c72373 Migrate slack notifications to GChat
Issue gh-15503

[skip ci]
2024-10-21 11:24:40 -05:00
Rob Winch 542071b1f8 Merge Add Firewall for WebFlux
Closes gh-15967
2024-10-21 08:56:42 -05:00
Rob Winch 4ce7cde155 Add Firewall for WebFlux
Closes gh-15967
2024-10-21 08:46:13 -05:00
dependabot[bot] c66ee4a21a Bump org.springframework.ldap:spring-ldap-core from 2.4.1 to 2.4.2
Bumps [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/spring-projects/spring-ldap/releases)
- [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt)
- [Commits](https://github.com/spring-projects/spring-ldap/compare/2.4.1...2.4.2)

---
updated-dependencies:
- dependency-name: org.springframework.ldap:spring-ldap-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-17 20:35:06 -07:00
dependabot[bot] 517ea7f42a Bump io.projectreactor.tools:blockhound
Bumps [io.projectreactor.tools:blockhound](https://github.com/reactor/BlockHound) from 1.0.9.RELEASE to 1.0.10.RELEASE.
- [Release notes](https://github.com/reactor/BlockHound/releases)
- [Commits](https://github.com/reactor/BlockHound/compare/1.0.9.RELEASE...1.0.10.RELEASE)

---
updated-dependencies:
- dependency-name: io.projectreactor.tools:blockhound
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 20:59:35 -07:00
dependabot[bot] fe79766aa2 Bump @antora/collector-extension in /docs
---
updated-dependencies:
- dependency-name: "@antora/collector-extension"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-13 18:08:50 -07:00
dependabot[bot] 755e9f2f69 Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.13 to 1.0.0-alpha.14.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.13...v1.0.0-alpha.14)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-22 17:55:03 -07:00
Steve Riesenberg 7b7a3044cf Polish gh-15533 2024-09-19 16:13:03 -05:00
陈圳佳 8a791028b1 Fix array values of additionalParameters
Closes gh-15468
2024-09-19 16:10:00 -05:00
Steve Riesenberg 0a4eb0f09a Update credential erasure examples
Closes gh-15683
2024-09-13 16:07:47 -05:00
Marcus Hert Da Coregio a0e6c17512 Do not log exception if CasJackson2Module is not present
Closes gh-15749
2024-09-09 08:32:40 -03:00
dependabot[bot] e251779b31 Bump @springio/asciidoctor-extensions in /docs
Bumps [@springio/asciidoctor-extensions](https://github.com/spring-io/asciidoctor-extensions) from 1.0.0-alpha.12 to 1.0.0-alpha.13.
- [Changelog](https://github.com/spring-io/asciidoctor-extensions/blob/main/CHANGELOG.adoc)
- [Commits](https://github.com/spring-io/asciidoctor-extensions/compare/v1.0.0-alpha.12...v1.0.0-alpha.13)

---
updated-dependencies:
- dependency-name: "@springio/asciidoctor-extensions"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-08 18:12:55 -07:00
dependabot[bot] 7ad6f38d5b Bump org-eclipse-jetty from 9.4.55.v20240627 to 9.4.56.v20240826
Bumps `org-eclipse-jetty` from 9.4.55.v20240627 to 9.4.56.v20240826.

Updates `org.eclipse.jetty:jetty-server` from 9.4.55.v20240627 to 9.4.56.v20240826

Updates `org.eclipse.jetty:jetty-servlet` from 9.4.55.v20240627 to 9.4.56.v20240826

---
updated-dependencies:
- dependency-name: org.eclipse.jetty:jetty-server
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.eclipse.jetty:jetty-servlet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-03 20:40:27 -07:00
Josh Cummings 97cefa6830 Update Formatting
Issue gh-15714
2024-09-03 15:32:59 -07:00
tugjg f836efb912 Address unnecessary method invocation
Closes gh-15714
2024-09-03 15:32:59 -07:00
Marcus Hert Da Coregio e92a945a2d Replace Env Variable with Expression
Issue gh-15659
2024-08-22 10:17:55 -03:00
Steve Riesenberg 92809cef01 Migrate slack notifications to GChat (2nd attempt)
Closes gh-15503
2024-08-21 15:44:44 -05:00
71 changed files with 2379 additions and 833 deletions
@@ -1,23 +0,0 @@
name: Clean build artifacts
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Delete artifacts in cron job
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
echo "Running clean build artifacts logic"
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
echo Output is $output
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
@@ -1,279 +0,0 @@
name: CI
on:
push:
branches-ignore:
- "dependabot/**"
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
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 }}
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
permissions:
contents: read
jobs:
prerequisites:
name: Pre-requisites for building
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
outputs:
runjobs: ${{ steps.continue.outputs.runjobs }}
project_version: ${{ steps.continue.outputs.project_version }}
steps:
- uses: actions/checkout@v4
- id: continue
name: Determine if should continue
run: |
# Run jobs if in upstream repository
echo "runjobs=true" >>$GITHUB_OUTPUT
# Extract version from gradle.properties
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
echo "project_version=$version" >>$GITHUB_OUTPUT
build_jdk_11:
name: Build JDK 11
needs: [prerequisites]
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: 'gradle'
- name: Set up Gradle
uses: gradle/gradle-build-action@v3
- name: Set up gradle user name
run: echo 'systemProp.user.name=spring-builds+github' >> gradle.properties
- name: Build with Gradle
run: ./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
snapshot_tests:
name: Test against snapshots
needs: [prerequisites]
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Snapshot Tests
run: ./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='5.+' -PreactorVersion='2020.0.+' -PspringDataVersion='2021.2.+' -PlocksDisabled --stacktrace
check_samples:
name: Check Samples project
needs: [prerequisites]
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Check samples project
env:
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
SAMPLES_DIR: ../spring-security-samples
VERSION: ${{ needs.prerequisites.outputs.project_version }}
run: |
./gradlew publishMavenJavaPublicationToLocalRepository
./gradlew cloneSamples -PcloneOutputDirectory="$SAMPLES_DIR"
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$VERSION" :runAllTests
check_tangles:
name: Check for Package Tangles
needs: [ prerequisites ]
runs-on: ubuntu-latest
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Check for package tangles
run: ./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
deploy_artifacts:
name: Deploy Artifacts
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Deploy artifacts
run: ./gradlew publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
deploy_docs:
name: Deploy Docs
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Deploy Docs
run: ./gradlew deployDocs -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
env:
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_HOST: ${{ secrets.DOCS_HOST }}
deploy_schema:
name: Deploy Schema
needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Deploy Schema
run: ./gradlew deploySchema -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace --info
env:
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_HOST: ${{ secrets.DOCS_HOST }}
perform_release:
name: Perform release
needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema]
runs-on: ubuntu-latest
permissions:
contents: write
timeout-minutes: 90
if: ${{ !endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }}
env:
REPO: ${{ github.repository }}
BRANCH: ${{ github.ref_name }}
TOKEN: ${{ github.token }}
VERSION: ${{ needs.prerequisites.outputs.project_version }}
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Wait for Artifactory Artifacts
if: ${{ contains(needs.prerequisites.outputs.project_version, '-RC') || contains(needs.prerequisites.outputs.project_version, '-M') }}
run: |
echo "Wait for artifacts of $REPO@$VERSION to appear on Artifactory."
until curl -f -s https://repo.spring.io/artifactory/milestone/org/springframework/security/spring-security-core/$VERSION/ > /dev/null
do
sleep 30
echo "."
done
echo "Artifacts for $REPO@$VERSION have been released to Artifactory."
- name: Wait for Maven Central Artifacts
if: ${{ !contains(needs.prerequisites.outputs.project_version, '-RC') && !contains(needs.prerequisites.outputs.project_version, '-M') }}
run: |
echo "Wait for artifacts of $REPO@$VERSION to appear on Maven Central."
until curl -f -s https://repo1.maven.org/maven2/org/springframework/security/spring-security-core/$VERSION/ > /dev/null
do
sleep 30
echo "."
done
echo "Artifacts for $REPO@$VERSION have been released to Maven Central."
- name: Create GitHub Release
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"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Setup git config
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Update to next Snapshot Version
run: |
echo "Updating $REPO@$VERSION to next snapshot version."
./gradlew :updateToSnapshotVersion
git commit -am "Next development version"
git push
perform_post_release:
name: Perform post-release
needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema]
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
timeout-minutes: 90
if: ${{ endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }}
env:
TOKEN: ${{ github.token }}
VERSION: ${{ needs.prerequisites.outputs.project_version }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
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()
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
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
-33
View File
@@ -1,33 +0,0 @@
name: Deploy Docs
on:
push:
branches-ignore:
- "gh-pages"
- "dependabot/**"
tags: '**'
repository_dispatch:
types: request-build-reference # legacy
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch:
permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
if: github.repository_owner == 'spring-projects'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: docs-build
fetch-depth: 1
- name: Dispatch (partial build)
if: github.ref_type == 'branch'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
- name: Dispatch (full build)
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
@@ -1,46 +0,0 @@
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:
DEPENDENCY_NAME: ${{ steps.extract.outputs.dependency_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "$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
-52
View File
@@ -1,52 +0,0 @@
name: Merge Dependabot PR
on: pull_request_target
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions: write-all
jobs:
merge-dependabot-pr:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Set Milestone to Dependabot Pull Request
id: set-milestone
run: |
if test -f pom.xml
then
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
else
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
fi
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
if [ -z $MILESTONE ]
then
gh run cancel ${{ github.run_id }}
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
else
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
echo mergeEnabled=true >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot pull request
if: steps.set-milestone.outputs.mergeEnabled
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
@@ -1,40 +0,0 @@
name: Check Milestone
on:
milestone:
types: [created, opened, edited]
env:
DUE_ON: ${{ github.event.milestone.due_on }}
TITLE: ${{ github.event.milestone.title }}
permissions:
contents: read
jobs:
spring-releasetrain-checks:
name: Check DueOn is on a Release Date
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Print Milestone Being Checked
run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'"
- name: Validate DueOn
if: env.DUE_ON != ''
run: |
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()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
uses: Gamesight/slack-workflow-status@v1.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
-44
View File
@@ -1,44 +0,0 @@
name: PR Build
on: pull_request
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Build with Gradle
run: ./gradlew clean build -PskipCheckExpectedBranchVersion --continue
generate-docs:
name: Generate Docs
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Run Antora
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@v4
with:
name: docs
path: docs/build/site
overwrite: true
@@ -1,22 +0,0 @@
name: Trigger Dependabot Auto Merge Forward
on:
push:
branches:
- '*.x'
permissions: read-all
jobs:
trigger-worflow:
name: Trigger Workflow
runs-on: ubuntu-latest
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- id: trigger
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run dependabot-auto-merge-forward.yml -r main
@@ -1,69 +0,0 @@
name: Update Scheduled Release Version
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:
contents: read
jobs:
update_scheduled_release_version:
name: Initiate Release If Scheduled
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- id: checkout-source
name: Checkout Source Code
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v2
with:
java-version: '11'
distribution: 'adopt'
- id: check-release-due
name: Check Release Due
run: |
./gradlew gitHubCheckNextVersionDueToday
echo "is_due_today=$(cat build/github/milestones/is-due-today)" >>$GITHUB_OUTPUT
- id: check-open-issues
name: Check for open issues
if: steps.check-release-due.outputs.is_due_today == 'true'
run: |
./gradlew gitHubCheckMilestoneHasNoOpenIssues
echo "is_open_issues=$(cat build/github/milestones/is-open-issues)" >>$GITHUB_OUTPUT
- id: validate-release-state
name: Validate State of Release
if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'true'
run: |
echo "The release is due today but there are open issues"
exit 1
- id: update-version-and-push
name: Update version and push
if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'false'
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
./gradlew :updateProjectVersion
updatedVersion=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
git commit -am "Release $updatedVersion"
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
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
@@ -51,7 +51,7 @@ public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
String version = (String) project.getVersion();
String branchVersion = getBranchVersion(project);
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
System.out.println("Branch version does not match *.x, ignoring");
System.out.println("Branch version '" + branchVersion + "' does not match *.x, ignoring");
return;
}
if (!versionsMatch(version, branchVersion)) {
@@ -67,7 +67,7 @@ public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
exec.setErrorOutput(System.err);
exec.setStandardOutput(baos);
});
return baos.toString();
return baos.toString().trim();
}
}
@@ -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());
}
/**
@@ -628,7 +628,8 @@ public abstract class AbstractRequestMatcherRegistry<C> {
public boolean matches(HttpServletRequest request) {
String name = request.getHttpServletMapping().getServletName();
ServletRegistration registration = this.servletContext.getServletRegistration(name);
Assert.notNull(registration, computeErrorMessage(this.servletContext.getServletRegistrations().values()));
Assert.notNull(registration,
() -> computeErrorMessage(this.servletContext.getServletRegistrations().values()));
try {
Class<?> clazz = Class.forName(registration.getClassName());
return DispatcherServlet.class.isAssignableFrom(clazz);
@@ -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)
@@ -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() {
@@ -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 {
@@ -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);
@@ -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;
@@ -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);
}
}
@@ -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 {
@@ -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;
}
@@ -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);
}
@@ -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;
+1 -1
View File
@@ -30,7 +30,7 @@ urls:
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
snapshot: true
runtime:
log:
+1
View File
@@ -160,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]
@@ -810,6 +810,8 @@ class WebSocketSecurityConfig: WebSocketMessageBrokerConfigurer {
----
======
This also implies that you will need to xref:servlet/integrations/websocket.adoc#websocket-authorization[use a `MessageMatcherDelegationAuthorizationManager`] to specify your authorization rules.
[[servlet-authorizationmanager-messages-opt-out]]
=== Opt-out Steps
@@ -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.
@@ -14,6 +14,7 @@ The picture below shows the typical layering of the handlers for a single HTTP r
.FilterChain
[[servlet-filterchain-figure]]
[.invert-dark]
image::{figures}/filterchain.png[]
The client sends a request to the application, and the container creates a `FilterChain` which contains the ``Filter``s and `Servlet` that should process the `HttpServletRequest` based on the path of the request URI.
@@ -67,6 +68,7 @@ Here is a picture of how `DelegatingFilterProxy` fits into the <<servlet-filters
.DelegatingFilterProxy
[[servlet-delegatingfilterproxy-figure]]
[.invert-dark]
image::{figures}/delegatingfilterproxy.png[]
`DelegatingFilterProxy` looks up __Bean Filter~0~__ from the `ApplicationContext` and then invokes __Bean Filter~0~__.
@@ -115,6 +117,7 @@ Since `FilterChainProxy` is a Bean, it is typically wrapped in a <<servlet-deleg
.FilterChainProxy
[[servlet-filterchainproxy-figure]]
[.invert-dark]
image::{figures}/filterchainproxy.png[]
[[servlet-securityfilterchain]]
@@ -124,6 +127,7 @@ image::{figures}/filterchainproxy.png[]
.SecurityFilterChain
[[servlet-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/securityfilterchain.png[]
The <<servlet-security-filters,Security Filters>> in `SecurityFilterChain` are typically Beans, but they are registered with `FilterChainProxy` instead of <<servlet-delegatingfilterproxy>>.
@@ -146,6 +150,7 @@ This allows providing a totally separate configuration for different _slices_ of
.Multiple SecurityFilterChain
[[servlet-multi-securityfilterchain-figure]]
[.invert-dark]
image::{figures}/multi-securityfilterchain.png[]
In the <<servlet-multi-securityfilterchain-figure>> Figure `FilterChainProxy` decides which `SecurityFilterChain` should be used.
@@ -391,6 +396,7 @@ The {security-api-url}org/springframework/security/web/access/ExceptionTranslati
`ExceptionTranslationFilter` is inserted into the <<servlet-filterchainproxy>> as one of the <<servlet-security-filters>>.
[.invert-dark]
image::{figures}/exceptiontranslationfilter.png[]
@@ -22,6 +22,7 @@ This also gives a good idea of the high level flow of authentication and how pie
At the heart of Spring Security's authentication model is the `SecurityContextHolder`.
It contains the <<servlet-authentication-securitycontext>>.
[.invert-dark]
image::{figures}/securitycontextholder.png[]
The `SecurityContextHolder` is where Spring Security stores the details of who is xref:features/authentication/index.adoc#authentication[authenticated].
@@ -171,6 +172,7 @@ While the implementation of `AuthenticationManager` could be anything, the most
Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide.
If none of the configured ``AuthenticationProvider``s can authenticate, then authentication will fail with a `ProviderNotFoundException` which is a special `AuthenticationException` that indicates the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it.
[.invert-dark]
image::{figures}/providermanager.png[]
In practice each `AuthenticationProvider` knows how to perform a specific type of authentication.
@@ -180,11 +182,13 @@ This allows each `AuthenticationProvider` to do a very specific type of authenti
`ProviderManager` also allows configuring an optional parent `AuthenticationManager` which is consulted in the event that no `AuthenticationProvider` can perform authentication.
The parent can be any type of `AuthenticationManager`, but it is often an instance of `ProviderManager`.
[.invert-dark]
image::{figures}/providermanager-parent.png[]
In fact, multiple `ProviderManager` instances might share the same parent `AuthenticationManager`.
This is somewhat common in scenarios where there are multiple xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] instances that have some authentication in common (the shared parent `AuthenticationManager`), but also different authentication mechanisms (the different `ProviderManager` instances).
[.invert-dark]
image::{figures}/providermanagers-parent.png[]
[[servlet-authentication-providermanager-erasing-credentials]]
@@ -230,6 +234,7 @@ Before the credentials can be authenticated, Spring Security typically requests
Next, the `AbstractAuthenticationProcessingFilter` can authenticate any authentication requests that are submitted to it.
[.invert-dark]
image::{figures}/abstractauthenticationprocessingfilter.png[]
image:{icondir}/number_1.png[] When the user submits their credentials, the `AbstractAuthenticationProcessingFilter` creates an <<servlet-authentication-authentication,`Authentication`>> from the `HttpServletRequest` to be authenticated.
@@ -9,6 +9,7 @@ Let's take a look at how HTTP Basic Authentication works within Spring Security.
First, we see the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client.
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/basicauthenticationentrypoint.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -26,6 +27,7 @@ Below is the flow for the username and password being processed.
[[servlet-authentication-basicauthenticationfilter]]
.Authenticating Username and Password
[.invert-dark]
image::{figures}/basicauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -8,6 +8,7 @@ Let's take a look at how `DaoAuthenticationProvider` works within Spring Securit
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] works.
.`DaoAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/daoauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[Reading the Username & Password] passes a `UsernamePasswordAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -10,6 +10,7 @@ Let's take a look at how form based log in works within Spring Security.
First, we see how the user is redirected to the log in form.
.Redirecting to the Log In Page
[.invert-dark]
image::{figures}/loginurlauthenticationentrypoint.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -30,6 +31,7 @@ When the username and password are submitted, the `UsernamePasswordAuthenticatio
The `UsernamePasswordAuthenticationFilter` extends xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[AbstractAuthenticationProcessingFilter], so this diagram should look pretty similar.
.Authenticating Username and Password
[.invert-dark]
image::{figures}/usernamepasswordauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -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()
}
}
----
=====
@@ -187,6 +187,7 @@ In Spring Security 6, the example shown above is the default configuration.
The {security-api-url}org/springframework/security/web/context/SecurityContextPersistenceFilter.html[`SecurityContextPersistenceFilter`] is responsible for persisting the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextpersistencefilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextPersistenceFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -208,6 +209,7 @@ To avoid these problems, the `SecurityContextPersistenceFilter` wraps both the `
The {security-api-url}org/springframework/security/web/context/SecurityContextHolderFilter.html[`SecurityContextHolderFilter`] is responsible for loading the `SecurityContext` between requests using the xref::servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`].
[.invert-dark]
image::{figures}/securitycontextholderfilter.png[]
image:{icondir}/number_1.png[] Before running the rest of the application, `SecurityContextHolderFilter` loads the `SecurityContext` from the `SecurityContextRepository` and sets it on the `SecurityContextHolder`.
@@ -73,6 +73,7 @@ For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` a
[[authz-authorization-manager-implementations]]
.Authorization Manager Implementations
[.invert-dark]
image::{figures}/authorizationhierarchy.png[]
Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision.
@@ -271,6 +272,7 @@ Whilst users can implement their own `AccessDecisionManager` to control all aspe
[[authz-access-voting]]
.Voting Decision Manager
[.invert-dark]
image::{figures}/access-decision-voting.png[]
Using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision.
@@ -330,6 +332,7 @@ For example, you'll find a https://spring.io/blog/2009/01/03/spring-security-cus
[[authz-after-invocation]]
.After Invocation Implementation
[.invert-dark]
image::{figures}/after-invocation.png[]
Like many other parts of Spring Security, `AfterInvocationManager` has a single concrete implementation, `AfterInvocationProviderManager`, which polls a list of ``AfterInvocationProvider``s.
@@ -46,6 +46,7 @@ Instead of the authentication needing to be looked up for every request, it will
When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`].
.Authorize HttpServletRequest
[.invert-dark]
image::{figures}/authorizationfilter.png[]
* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
@@ -12,6 +12,7 @@ The {security-api-url}org/springframework/security/web/access/intercept/FilterSe
It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters].
.Authorize HttpServletRequest
[.invert-dark]
image::{figures}/filtersecurityinterceptor.png[]
* image:{icondir}/number_1.png[] First, the `FilterSecurityInterceptor` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
@@ -208,6 +208,78 @@ This will ensure that:
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
[[migrating-spel-expressions]]
=== Migrating SpEL Expressions
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
However, to ease migration, you can also use a class like the following:
[source,java]
----
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
private Expression expression;
public MessageExpressionAuthorizationManager(String expressionString) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
return new ExpressionAuthorizationDecision(granted, this.expression);
}
}
----
And specify an instance for each matcher that you cannot get migrate:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
// ...
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
// ...
return messages.build();
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
messages
// ..
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
// ...
return messages.build()
}
}
----
======
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
@@ -21,6 +21,7 @@ Let's take a look at how Bearer Token Authentication works within Spring Securit
First, we see that, like xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client.
.Sending WWW-Authenticate Header
[.invert-dark]
image::{figures}/bearerauthenticationentrypoint.png[]
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -38,6 +39,7 @@ Below is the flow for the bearer token being processed.
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
.Authenticating Bearer Token
[.invert-dark]
image::{figures}/bearertokenauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
@@ -92,6 +92,7 @@ Let's take a look at how `JwtAuthenticationProvider` works within Spring Securit
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> works.
.`JwtAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/jwtauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -88,6 +88,7 @@ Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring
The figure explains details of how the xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationmanager[`AuthenticationManager`] in figures from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> works.
.`OpaqueTokenAuthenticationProvider` Usage
[.invert-dark]
image::{figures}/opaquetokenauthenticationprovider.png[]
image:{icondir}/number_1.png[] The authentication `Filter` from <<oauth2resourceserver-authentication-bearertokenauthenticationfilter,Reading the Bearer Token>> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`].
@@ -7,6 +7,7 @@ First, we see that, like xref:servlet/oauth2/login/index.adoc[OAuth 2.0 Login],
It does this through a series of redirects.
.Redirecting to Asserting Party Authentication
[.invert-dark]
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] and xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[`AbstractAuthenticationProcessingFilter`] diagrams:
@@ -29,6 +30,7 @@ image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
.Authenticating a `<saml2:Response>`
[.invert-dark]
image::{figures}/saml2webssoauthenticationfilter.png[]
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
+3 -3
View File
@@ -1,10 +1,10 @@
{
"dependencies": {
"antora": "3.2.0-alpha.6",
"antora": "3.2.0-alpha.8",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-beta.2",
"@antora/collector-extension": "1.0.1",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.12"
"@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>
+16
View File
@@ -29,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>
+1 -1
View File
@@ -1,5 +1,5 @@
springBootVersion=2.7.12
version=5.8.15-SNAPSHOT
version=5.8.17-SNAPSHOT
samplesBranch=5.8.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
+4 -4
View File
@@ -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.55.v20240627"
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"
@@ -32,7 +32,7 @@ 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.48"
io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2020.0.47"
io-projectreactor-tools-blockhound = "io.projectreactor.tools:blockhound:1.0.9.RELEASE"
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.3"
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" }
@@ -91,7 +91,7 @@ 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"
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -144,9 +145,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
*/
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;
}
/**
@@ -155,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;
}
@@ -364,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;
@@ -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);
};
@@ -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;
}
@@ -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);
}
@@ -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.
@@ -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)
@@ -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)
@@ -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();
}
@@ -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");
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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);
});
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
@@ -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
@@ -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) == '/');
}
@@ -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();
}
}
@@ -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
@@ -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();
}
}