8000 Tighten UNSAFE_PATH_PATTERN against encoded path-traversal terminators by kosiorkosa47 · Pull Request #49000 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Tighten UNSAFE_PATH_PATTERN against encoded path-traversal terminators - #49000

Merged
rmartinc merged 2 commits into
keycloak:mainfrom
kosiorkosa47:fix/48978-unsafe-path-pattern-encoded-terminators
May 21, 2026
Merged

rmartinc merged 2 commits into
keycloak:mainfrom
kosiorkosa47:fix/48978-unsafe-path-pattern-encoded-terminators

Conversation

@kosiorkosa47
Copy link
Copy Markdown
Contributor

Closes #48978

Summary

Extends UNSAFE_PATH_PATTERN in RedirectUtils to cover the percent-encoded
terminators and double-encoded dots that were noted in the issue as gaps:

  • %3B / %3b (encoded semicolon)
  • %09, %0A, %0D, %00 (control characters)
  • %252E (double-encoded dot)

The end-of-input anchor $ is moved into the terminator alternation, which
collapses the two pattern alternatives into one and keeps the diff small.

Why

These encoded forms do not produce actual path traversal on conformant
servers (per RFC 3986, percent-encoded characters in a path are literals,
not delimiters — confirmed empirically against Tomcat 9, nginx 1.27,
Apache httpd 2.4, Spring Boot 2.2, Node.js Express 5, and an nginx ↔ Tomcat
proxy chain). However, they're semantically close enough to the patterns
the regex was designed to block, and #48978 calls for the defense-in-depth
coverage.

Approach (Option A vs Option B)

This PR implements Option A from the issue — regex extension. It is
deliberately the smaller of the two suggested changes; canonical path
normalization (Option B) would be the more robust long-term answer but
involves broader code changes. Happy to do Option B as a follow-up PR if
preferred — let me know.

Test changes (RedirectUtilsTest.testEncodedRedirectUri)

8 new assertNull assertions for the encoded forms now blocked:

"https://keycloak.org/test/..%3B/"
"https://keycloak.org/test/..%3b/"
"https://keycloak.org/test/..%09/"
"https://keycloak.org/test/..%0A/"
"https://keycloak.org/test/..%0D/"
"https://keycloak.org/test/..%00/"
"https://keycloak.org/test/..%3Bsomething/"
"https://keycloak.org/test/%252E%252E/"
"https://keycloak.org/test/%252E%252E/#encodeTest=a%3Cb"

3 prior assertEquals flipped to assertNull — these patterns were
previously allowed and are now blocked by design:

URI Previously Now
https://keycloak.org/test/%252E%252E/ allowed blocked
https://keycloak.org/test/%252E%252E/#encodeTest=a%3Cb allowed blocked
https://keycloak.org/test/..%3Bsomething/ allowed blocked

1 new assertEquals (negative test) confirming %3B as legitimate
path content (not preceded by ..) still resolves:

"https://keycloak.org/test/file%3Bname/"

Triple-encoded variants (%25252E) remain allowed — out of scope
for this issue, would need recursive decoding (Option B territory).

Trade-off note for review

..%3Bsomething/ is now blocked because the regex matches ..%3B as
substring regardless of what follows (matching the existing
..;something/ behaviour for the literal-semicolon form, which is also
blocked — see line 258). If the maintainers prefer the more permissive
semantic ("only block when the terminator is immediately followed by
end-of-segment"), the regex can be tightened with a lookahead. Happy to
do that variant if it's the preferred direction.

Verification

Local run:

[INFO] Running org.keycloak.protocol.oidc.utils.RedirectUtilsTest
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0

Full module:

[INFO] Tests run: 456, Failures: 0, Errors: 0, Skipped: 2
[INFO] BUILD SUCCESS

No regressions in the services module test suite.

DCO

Signed off in commit (Signed-off-by: Michał Kosiorek <…>).

@kosiorkosa47
kosiorkosa47 requested a review from a team as a code owner May 14, 2026 15:27
@rmartinc rmartinc left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kosiorkosa47 There are failures in the testsuite. Probably now the double encoded should be moved to false here.

Initially I see this OK, but the test should be changed to make it pass OK.

@kosiorkosa47
Copy link
Copy Markdown
Contributor Author

CI caught a cross-module integration test that needed updating alongside the regex change. My local verification for the original commit was scoped to the services module (where RedirectUtilsTest lives); testsuite/integration-arquillian/tests/base was outside that scope, so the four pre-existing OAuthRedirectUriTest.testWildcard assertions for %252E%252E variants didn't flip with the regex.

Pushed 884a769506 flipping those four assertions to false to match the Option A semantic. Triple- and septuple-encoded variants remain true — recursive decoding is explicitly out of scope for #48978.

Verification:

  • mvn -pl services -Dtest=RedirectUtilsTest test → 11/11 green.
  • Direct regex match against the four flipped URIs confirms UNSAFE_PATH_PATTERN matches each rawPath, mechanically equivalent to the verifyRedirectUri path exercised by the arquillian test.

For future security-pattern changes I'll widen the local test scope to include testsuite/integration-arquillian/tests/base for anything touching redirect/URL validation.


Side note, not in scope of this PR: there's a near-identical UNSAFE_PATH_PATTERN in services/src/main/java/org/keycloak/protocol/oauth2/cimd/clientpolicy/executor/AbstractClientIdMetadataDocumentExecutor.java:705 used to validate URIs in OAuth2 client policy execution. It carries the same encoded-terminator gaps that #48978 addresses for RedirectUtils. Happy to open a follow-up issue if maintainers want the same hardening applied there.

@kosiorkosa47
Copy link
Copy Markdown
Contributor Author

@rmartinc Thanks for the review — your suggestion at L359-L362 matches the fix in 884a769506 that I pushed a few minutes earlier (we crossed in the air). Same intent, same lines. CI re-running on the new commit now.

@rmartinc rmartinc self-assigned this May 18, 2026
@rmartinc rmartinc left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kosiorkosa47! LGTM!
@mposolda do you want to review too?

Fixes keycloak#48978

Extends the regex to cover encoded forms that previously bypassed
detection:
  - %3B / %3b (encoded semicolon)
  - %09, %0A, %0D, %00 (control characters)
  - %252E (double-encoded dot)

These encodings do not produce actual path traversal on conformant
servers per RFC 3986 (percent-encoded characters are literals, not
delimiters), but are semantically close enough to the patterns the
regex was designed to block to warrant defense-in-depth coverage.

The end-of-input anchor ($) is moved into the terminator class to
collapse the two pattern alternatives into one, keeping the diff
minimal.

Test changes:
  - 8 new assertions covering encoded semicolons, control character
    terminators, and double-encoded dots.
  - 3 prior assertEquals flipped to assertNull (lines that previously
    asserted %252E%252E/, %252E%252E/#fragment, and ..%3Bsomething/
    were allowed are now expected to be blocked).
  - 1 new negative test confirming %3B as legitimate path content (not
    following a parent-folder sequence) still resolves.

Triple-encoded variants (e.g., %25252E) remain allowed; out of scope
for this issue.

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>
Follow-up to 36b0b10dd2 — Base IT (6) CI run
8000
 for keycloak#49000 caught a
cross-module integration test that needed updating alongside the
regex change. Local verification of the previous commit covered the
services module (RedirectUtilsTest); testsuite/integration-arquillian
was outside that scope, so the existing OAuthRedirectUriTest.testWildcard
expectations for %252E%252E variants didn't flip with the regex.

Four assertions in testWildcard flipped from true → false to match the
Option A semantic introduced in 36b0b10dd2 (double-encoded dots are
now blocked by UNSAFE_PATH_PATTERN):

  http://example.com/foo/%252E%252E/
  http://example.com/foo/%252E%252E/?some_query_param=some_value
  http://example.com/foo/%252E%252E/?encodeTest=a%3Cb
  http://example.com/foo/%252E%252E/#encodeTest=a%3Cb

Triple-encoded (%25252E) and septuple-encoded variants remain
expected:true — recursive decoding is explicitly out of scope for
keycloak#48978.

Verified locally:
  - mvn -pl services -Dtest=RedirectUtilsTest test → 11/11 green.
  - Direct regex match against the four flipped URIs confirms
    UNSAFE_PATH_PATTERN matches each rawPath, mechanically equivalent
    to the verifyRedirectUri code path exercised by the arquillian test.

Refs keycloak#48978

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>
@rmartinc
rmartinc force-pushed the fix/48978-unsafe-path-pattern-encoded-terminators branch from 884a769 to 7b26bd2 Compare May 21, 2026 08:42
@rmartinc
Copy link
Copy Markdown
Contributor

I'm rebasing this, because there were other changes in the same area. @mposolda If everything goes OK I will merge it. Last chance to take a look. 😄

@rmartinc
rmartinc merged commit c3f34c3 into keycloak:main May 21, 2026
87 checks passed
rmartinc pushed a commit to rmartinc/keycloak that referenced this pull request May 26, 2026
keycloak#49000)

* Tighten UNSAFE_PATH_PATTERN against encoded path-traversal terminators

Fixes keycloak#48978

Extends the regex to cover encoded forms that previously bypassed
detection:
  - %3B / %3b (encoded semicolon)
  - %09, %0A, %0D, %00 (control characters)
  - %252E (double-encoded dot)

These encodin
8000
gs do not produce actual path traversal on conformant
servers per RFC 3986 (percent-encoded characters are literals, not
delimiters), but are semantically close enough to the patterns the
regex was designed to block to warrant defense-in-depth coverage.

The end-of-input anchor ($) is moved into the terminator class to
collapse the two pattern alternatives into one, keeping the diff
minimal.

Test changes:
  - 8 new assertions covering encoded semicolons, control character
    terminators, and double-encoded dots.
  - 3 prior assertEquals flipped to assertNull (lines that previously
    asserted %252E%252E/, %252E%252E/#fragment, and ..%3Bsomething/
    were allowed are now expected to be blocked).
  - 1 new negative test confirming %3B as legitimate path content (not
    following a parent-folder sequence) still resolves.

Triple-encoded variants (e.g., %25252E) remain allowed; out of scope
for this issue.

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>

* Update OAuthRedirectUriTest expectations for double-encoded dots

Follow-up to 36b0b10dd2 — Base IT (6) CI run for keycloak#49000 caught a
cross-module integration test that needed updating alongside the
regex change. Local verification of the previous commit covered the
services module (RedirectUtilsTest); testsuite/integration-arquillian
was outside that scope, so the existing OAuthRedirectUriTest.testWildcard
expectations for %252E%252E variants didn't flip with the regex.

Four assertions in testWildcard flipped from true → false to match the
Option A semantic introduced in 36b0b10dd2 (double-encoded dots are
now blocked by UNSAFE_PATH_PATTERN):

  http://example.com/foo/%252E%252E/
  http://example.com/foo/%252E%252E/?some_query_param=some_value
  http://example.com/foo/%252E%252E/?encodeTest=a%3Cb
  http://example.com/foo/%252E%252E/#encodeTest=a%3Cb

Triple-encoded (%25252E) and septuple-encoded variants remain
expected:true — recursive decoding is explicitly out of scope for
keycloak#48978.

Verified locally:
  - mvn -pl services -Dtest=RedirectUtilsTest test → 11/11 green.
  - Direct regex match against the four flipped URIs confirms
    UNSAFE_PATH_PATTERN matches each rawPath, mechanically equivalent
    to the verifyRedirectUri code path exercised by the arquillian test.

Refs keycloak#48978

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>

---------

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>
(cherry picked from commit c3f34c3)
ahus1 pushed a commit that referenced this pull request May 26, 2026
Fixes #48978

Extends the regex to cover encoded forms that previously bypassed
detection:
  - %3B / %3b (encoded semicolon)
  - %09, %0A, %0D, %00 (control characters)
  - %252E (double-encoded dot)

These encodings do not produce actual path traversal on conformant
servers per RFC 3986 (percent-encoded characters are literals, not
delimiters), but are semantically close enough to the patterns the
regex was designed to block to warrant defense-in-depth coverage.

The end-of-input anchor ($) is moved into the terminator class to
collapse the two pattern alternatives into one, keeping the diff
minimal.

Test changes:
  - 8 new assertions covering encoded semicolons, control character
    terminators, and double-encoded dots.
  - 3 prior assertEquals flipped to assertNull (lines that previously
    asserted %252E%252E/, %252E%252E/#fragment, and ..%3Bsomething/
    were allowed are now expected to be blocked).
  - 1 new negative test confirming %3B as legitimate path content (not
    following a parent-folder sequence) still resolves.

Triple-encoded variants (e.g., %25252E) remain allowed; out of scope
for this issue.



* Update OAuthRedirectUriTest expectations for double-encoded dots

Follow-up to 36b0b10dd2 — Base IT (6) CI run for #49000 caught a
cross-module integration test that needed updating alongside the
regex change. Local verification of the previous commit covered the
services module (RedirectUtilsTest); testsuite/integration-arquillian
was outside that scope, so the existing OAuthRedirectUriTest.testWildcard
expectations for %252E%252E variants didn't flip with the regex.

Four assertions in testWildcard flipped from true → false to match the
Option A semantic introduced in 36b0b10dd2 (double-encoded dots are
now blocked by UNSAFE_PATH_PATTERN):

  http://example.com/foo/%252E%252E/
  http://example.com/foo/%252E%252E/?some_query_param=some_value
  http://example.com/foo/%252E%252E/?encodeTest=a%3Cb
  http://example.com/foo/%252E%252E/#encodeTest=a%3Cb

Triple-encoded (%25252E) and septuple-encoded variants remain
expected:true — recursive decoding is explicitly out of scope for
#48978.

Verified locally:
  - mvn -pl services -Dtest=RedirectUtilsTest test → 11/11 green.
  - Direct regex match against the four flipped URIs confirms
    UNSAFE_PATH_PATTERN matches each rawPath, mechanically equivalent
    to the verifyRedirectUri code path exercised by the arquillian test.

Refs #48978



---------


(cherry picked from commit c3f34c3)

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>
Co-authored-by: Kosiorkosa47 <mkosiorek121@gmail.com>
@stianst stianst mentioned this pull request Jun 1, 2026
sashyo pushed a commit to tide-foundation/keycloak-IGA that referenced this pull request Jul 20, 2026
keycloak#49000)

* Tighten UNSAFE_PATH_PATTERN against encoded path-traversal terminators

Fixes keycloak#48978

Extends the regex to cover encoded forms that previously bypassed
detection:
  - %3B / %3b (encoded semicolon)
  - %09, %0A, %0D, %00 (control characters)
  - %252E (double-encoded dot)

These encodings do not produce actual path traversal on conformant
servers per RFC 3986 (percent-encoded characters are literals, not
delimiters), but are semantically close enough to the patterns the
regex was designed to block to warrant defense-in-depth coverage.

The end-of-input anchor ($) is moved into the terminator class to
collapse the two pattern alternatives into one, keeping the diff
minimal.

Test changes:
  - 8 new assertions covering encoded semicolons, control character
    terminators, and double-encoded dots.
  - 3 prior assertEquals flipped to assertNull (lines that previously
    asserted %252E%252E/, %252E%252E/#fragment, and ..%3Bsomething/
    were allowed are now expected to be blocked).
  - 1 new negative test confirming %3B as legitimate path content (not
    following a parent-folder sequence) still resolves.

Triple-encoded variants (e.g., %25252E) remain allowed; out of scope
for this issue.

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>

* Update OAuthRedirectUriTest expectations for double-encoded dots

Follow-up to 36b0b10dd2 — Base IT (6) CI run for keycloak#49000 caught a
cross-module integration test that needed updating alongside the
regex change. Local verification of the previous commit covered the
services module (RedirectUtilsTest); testsuite/integration-arquillian
was outside that scope, so the existing OAuthRedirectUriTest.testWildcard
expectations for %252E%252E variants didn't flip with the regex.

Four assertions in testWildcard flipped from true → false to match the
Option A semantic introduced in 36b0b10dd2 (double-encoded dots are
now blocked by UNSAFE_PATH_PATTERN):

  http://example.com/foo/%252E%252E/
  http://example.com/foo/%252E%252E/?some_query_param=some_value
  http://example.com/foo/%252E%252E/?encodeTest=a%3Cb
  http://example.com/foo/%252E%252E/#encodeTest=a%3Cb

Triple-encoded (%25252E) and septuple-encoded variants remain
expected:true — recursive decoding is explicitly out of scope for
keycloak#48978.

Verified locally:
  - mvn -pl services -Dtest=RedirectUtilsTest test → 11/11 green.
  - Direct regex match against the four flipped URIs confirms
    UNSAFE_PATH_PATTERN matches each rawPath, mechanically equivalent
    to the verifyRedirectUri code path exercised by the arquillian test.

Refs keycloak#48978

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>

---------

Signed-off-by: Michał Kosiorek <michal.kosiorek@arklink.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UNSAFE_PATH_PATTERN regex to cover percent-encoded terminators and control characters

3 participants

0