Repository navigation
Support a caller-supplied redirect on /reset-remember-wayf - #2097
kayjoosten wants to merge 4 commits into
Conversation
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
3081177 to
a362211
Compare
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
924a15c to
d64ba78
Compare
johanib
left a comment
There was a problem hiding this comment.
Looks good.
I think AI still found a valid looking case that seems to reproduce.
Seems good to close that hole and apply the basic sanitization.
|
|
||
| final class ResetRememberedWayfController | ||
| { | ||
| private const ALLOWED_REDIRECT_SCHEMES = ['http', 'https']; |
There was a problem hiding this comment.
Is http needed? devconf runs on https. So just for github pipeline, or?
There was a problem hiding this comment.
Not needed, you are right. Only https is accepted now (ff0404b), the unit test covers a plain http URL being rejected.
| return $this->redirectUrl; | ||
| } | ||
|
|
||
| $parts = parse_url($redirect); |
There was a problem hiding this comment.
Reject browser-normalized backslashes before allowlisting
parse_url() reports https://evil.example.org\@profile.example.org/ as having the allowed host profile.example.org, so this branch returns it. Browsers normalize the backslash to / and navigate to evil.example.org, allowing an attacker to bypass the host allowlist and turn the endpoint into an open redirect.
Relevant lines: /home/johan/project/surf/OpenConext-engineblock/.worktrees/pr-review-2097/src/OpenConext/EngineBlockBundle/Controller/ResetRememberedWayfController.php:74-88
Suggested approach: Use browser-compatible URL parsing and canonicalization, or reject backslashes and other ambiguous authority characters before comparing the host; only return the canonical URL after validation.
There was a problem hiding this comment.
Fixed in ff0404b. Values containing a backslash, whitespace or control characters are rejected before parse_url(), URLs with userinfo are rejected, and only https is allowed. Unit tests cover the backslash bypass from this comment, userinfo, tab/newline, a leading space, protocol-relative URLs and a lookalike host. Docs and changelog are updated.
a362211 to
0f9a300
Compare
d64ba78 to
51cb8a0
Compare
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
Let /reset-remember-wayf accept an optional `redirect` query parameter so callers such as Profile can send the user back to the page they came from once their remembered per-SP WAYF choices are cleared, instead of always sending everyone to the single operator-configured URL from #2085. Every redirect (whether the caller-supplied target or the configured fallback) now also carries a `wayfReset=removed|none` query parameter, so the caller can tell whether a cookie actually existed and was cleared. - New config wayf.reset_choice_allowed_redirect_hosts is a host allowlist (defaulting to profile.dev.openconext.local in dev/CI): the `redirect` parameter is only honoured when it parses to an http(s) URL whose host appears in this list, otherwise the endpoint silently falls back to the existing wayf.reset_choice_per_idp_redirect target. This mirrors the "parse and check an allowed value" shape of the existing AllowedSchemeValidator rather than introducing a new general-purpose URL validation abstraction for a single call site. - Resolving the redirect target and appending the wayfReset signal are both extracted into small private methods so the __invoke method reads as a straight sequence: clear/log the cookie if present, resolve where to send the user, tell them what happened. - The entry-count/log-message behaviour from #2085 is unchanged: a present-but-unparseable cookie is still treated the same as a valid one for logging and for the wayfReset signal (both are cleared and reported), since from the caller's perspective the cookie no longer exists either way. - Existing unit/functional tests were updated for the new `?wayfReset=` suffix on every redirect, and new cases cover an allowed redirect host, a redirect with its own query string, a disallowed host, a malformed URL, and a disallowed scheme. Needed by OpenConext/OpenConext-profile#345, which links back to Profile's "my personal data" page after resetting a user's remembered WAYF choices.
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
# Why is this change needed? Prior to this change, the redirect query parameter, the allowed redirect hosts and the wayfReset result parameter of /reset-remember-wayf were only described in parameters.yml.dist. Operators need to know about the open redirect protection and integrators about the result parameter. # How does it address the issue? This change extends the reset section in docs/wayf_remember_choice.md and adds an entry to the CHANGELOG.
0f9a300 to
a7aacbc
Compare
51cb8a0 to
357be6c
Compare
# If applied, this commit will... Only accept https redirects and reject backslashes, control characters, whitespace and userinfo in the redirect query parameter. # Why is this change needed? Prior to this change, the redirect value was only checked with parse_url() against the host allowlist. Browsers normalise a value such as https://evil.example.org\@profile.example.org/ differently from parse_url(), so the host check could pass while the browser navigated to another host (open redirect). Plain http was also accepted without a need for it. # How does it address the issue? This change rejects values containing a backslash, control character or whitespace, values with credentials, and any scheme other than https. Unit tests cover each of these cases and the docs and changelog describe the stricter rules. # Provide links to any relevant tickets, articles or other resources Review feedback on the redirect parameter pull request.
What
Extends
GET /reset-remember-wayf(introduced in #2085 / #2094) so itaccepts an optional
redirectquery parameter, and always appends awayfReset=removed|nonequery parameter to whichever redirect target isused. This is a prerequisite for
OpenConext/OpenConext-profile#345,
which wants to link from Profile's "my personal data" page to this
endpoint and land the user back on that same page afterwards, knowing
whether a cookie was actually cleared.
Stacked on #2094
This branches from
wayf-reset-endpoint(#2094, not yet merged), since itdirectly extends
ResetRememberedWayfControlleradded there. The diffshown here is the incremental change only; once #2094 merges to
main,this PR's base should be retargeted to
main.Details
wayf.reset_choice_allowed_redirect_hostsis a hostallowlist (defaulting to
profile.dev.openconext.localin dev/CI). Theredirectparameter is only honoured when it parses to anhttp(s)URLwhose host appears in this list; otherwise the endpoint silently falls
back to the existing
wayf.reset_choice_per_idp_redirecttarget. Thismirrors the "parse and check an allowed value" shape of the existing
AllowedSchemeValidatorrather than introducing a new general-purposeURL validation abstraction for a single call site.
wayfResetsignal areextracted into small private methods, so
__invokereads as a straightsequence: clear/log the cookie if present, resolve where to send the
user, tell them what happened.
present-but-unparseable cookie is still treated the same as a valid one
for logging and for the
wayfResetsignal (both are cleared andreported), since from the caller's perspective the cookie no longer
exists either way.
?wayfReset=suffix on every redirect, and new cases cover an allowed redirect host,
a redirect with its own query string, a disallowed host, a malformed
URL, and a disallowed scheme.
Testing
All run inside the Docker dev container (PHP 8.5):
phpmd/phpcs/phpcs-legacy/docheader: clean.eb4: 244/244.unit: 1008/1008 (1003 baseline + 5 new).functional(
APP_ENV=test): 123/123 (121 baseline + 2 new).integration:105/105.
defaultsuite: 300/300 scenarios, 5557/5557 steps (unchanged —no new scenarios needed, this is pure PHPUnit-covered logic).
Refs: OpenConext/OpenConext-profile#345