mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-28 06:47:37 +00:00
Fix open redirect in legacy SSO flow (idp) (#18909)
Some checks are pending
Build docker images / Build and push image for ${{ matrix.platform }} (linux/amd64, ubuntu-24.04, linux-amd64) (push) Waiting to run
Build docker images / Build and push image for ${{ matrix.platform }} (linux/arm64, ubuntu-24.04-arm, linux-arm64) (push) Waiting to run
Build docker images / Push merged images to ${{ matrix.repository }} (docker.io/matrixdotorg/synapse) (push) Blocked by required conditions
Build docker images / Push merged images to ${{ matrix.repository }} (ghcr.io/element-hq/synapse) (push) Blocked by required conditions
Deploy the documentation / Calculate variables for GitHub Pages deployment (push) Waiting to run
Deploy the documentation / GitHub Pages (push) Blocked by required conditions
Build release artifacts / Calculate list of debian distros (push) Waiting to run
Build release artifacts / Build .deb packages (push) Blocked by required conditions
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, macos-13) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, macos-14) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, ubuntu-24.04) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, ubuntu-24.04-arm) (push) Waiting to run
Build release artifacts / Build sdist (push) Waiting to run
Build release artifacts / Attach assets to release (push) Blocked by required conditions
Schema / Ensure Synapse config schema is valid (push) Waiting to run
Schema / Ensure generated documentation is up-to-date (push) Waiting to run
Tests / changes (push) Waiting to run
Tests / check-sampleconfig (push) Blocked by required conditions
Tests / check-schema-delta (push) Blocked by required conditions
Tests / check-lockfile (push) Waiting to run
Tests / lint (push) Blocked by required conditions
Tests / Typechecking (push) Blocked by required conditions
Tests / lint-crlf (push) Waiting to run
Tests / lint-newsfile (push) Waiting to run
Tests / lint-pydantic (push) Blocked by required conditions
Tests / lint-clippy (push) Blocked by required conditions
Tests / lint-clippy-nightly (push) Blocked by required conditions
Tests / lint-rust (push) Blocked by required conditions
Tests / lint-rustfmt (push) Blocked by required conditions
Tests / lint-readme (push) Blocked by required conditions
Tests / linting-done (push) Blocked by required conditions
Tests / calculate-test-jobs (push) Blocked by required conditions
Tests / trial (push) Blocked by required conditions
Tests / trial-olddeps (push) Blocked by required conditions
Tests / trial-pypy (all, pypy-3.9) (push) Blocked by required conditions
Tests / sytest (push) Blocked by required conditions
Tests / export-data (push) Blocked by required conditions
Tests / portdb (13, 3.9) (push) Blocked by required conditions
Tests / portdb (17, 3.13) (push) Blocked by required conditions
Tests / complement (monolith, Postgres) (push) Blocked by required conditions
Tests / complement (monolith, SQLite) (push) Blocked by required conditions
Tests / complement (workers, Postgres) (push) Blocked by required conditions
Tests / cargo-test (push) Blocked by required conditions
Tests / cargo-bench (push) Blocked by required conditions
Tests / tests-done (push) Blocked by required conditions
Some checks are pending
Build docker images / Build and push image for ${{ matrix.platform }} (linux/amd64, ubuntu-24.04, linux-amd64) (push) Waiting to run
Build docker images / Build and push image for ${{ matrix.platform }} (linux/arm64, ubuntu-24.04-arm, linux-arm64) (push) Waiting to run
Build docker images / Push merged images to ${{ matrix.repository }} (docker.io/matrixdotorg/synapse) (push) Blocked by required conditions
Build docker images / Push merged images to ${{ matrix.repository }} (ghcr.io/element-hq/synapse) (push) Blocked by required conditions
Deploy the documentation / Calculate variables for GitHub Pages deployment (push) Waiting to run
Deploy the documentation / GitHub Pages (push) Blocked by required conditions
Build release artifacts / Calculate list of debian distros (push) Waiting to run
Build release artifacts / Build .deb packages (push) Blocked by required conditions
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, macos-13) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, macos-14) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, ubuntu-24.04) (push) Waiting to run
Build release artifacts / Build wheels on ${{ matrix.os }} (${{ startsWith(github.ref, 'refs/pull/') }}, ubuntu-24.04-arm) (push) Waiting to run
Build release artifacts / Build sdist (push) Waiting to run
Build release artifacts / Attach assets to release (push) Blocked by required conditions
Schema / Ensure Synapse config schema is valid (push) Waiting to run
Schema / Ensure generated documentation is up-to-date (push) Waiting to run
Tests / changes (push) Waiting to run
Tests / check-sampleconfig (push) Blocked by required conditions
Tests / check-schema-delta (push) Blocked by required conditions
Tests / check-lockfile (push) Waiting to run
Tests / lint (push) Blocked by required conditions
Tests / Typechecking (push) Blocked by required conditions
Tests / lint-crlf (push) Waiting to run
Tests / lint-newsfile (push) Waiting to run
Tests / lint-pydantic (push) Blocked by required conditions
Tests / lint-clippy (push) Blocked by required conditions
Tests / lint-clippy-nightly (push) Blocked by required conditions
Tests / lint-rust (push) Blocked by required conditions
Tests / lint-rustfmt (push) Blocked by required conditions
Tests / lint-readme (push) Blocked by required conditions
Tests / linting-done (push) Blocked by required conditions
Tests / calculate-test-jobs (push) Blocked by required conditions
Tests / trial (push) Blocked by required conditions
Tests / trial-olddeps (push) Blocked by required conditions
Tests / trial-pypy (all, pypy-3.9) (push) Blocked by required conditions
Tests / sytest (push) Blocked by required conditions
Tests / export-data (push) Blocked by required conditions
Tests / portdb (13, 3.9) (push) Blocked by required conditions
Tests / portdb (17, 3.13) (push) Blocked by required conditions
Tests / complement (monolith, Postgres) (push) Blocked by required conditions
Tests / complement (monolith, SQLite) (push) Blocked by required conditions
Tests / complement (workers, Postgres) (push) Blocked by required conditions
Tests / cargo-test (push) Blocked by required conditions
Tests / cargo-bench (push) Blocked by required conditions
Tests / tests-done (push) Blocked by required conditions
- Validate the `idp` parameter to only accept the ones that are known in the config file - URL-encode the `idp` parameter for safety's sake (this is the main fix) Fix https://github.com/matrix-org/internal-config/issues/1651 (internal link) Regressed in https://github.com/element-hq/synapse/pull/17972
This commit is contained in:
parent
84d64251dc
commit
6f9fab1089
1
changelog.d/18909.bugfix
Normal file
1
changelog.d/18909.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fix open redirect in legacy SSO flow with the `idp` query parameter.
|
||||
@ -22,6 +22,7 @@
|
||||
"""Contains the URL paths to prefix various aspects of the server with."""
|
||||
|
||||
import hmac
|
||||
import urllib.parse
|
||||
from hashlib import sha256
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode, urljoin
|
||||
@ -96,11 +97,21 @@ class LoginSSORedirectURIBuilder:
|
||||
serialized_query_parameters = urlencode({"redirectUrl": client_redirect_url})
|
||||
|
||||
if idp_id:
|
||||
# Since this is a user-controlled string, make it safe to include in a URL path.
|
||||
url_encoded_idp_id = urllib.parse.quote(
|
||||
idp_id,
|
||||
# Since this defaults to `safe="/"`, we have to override it. We're
|
||||
# working with an individual URL path parameter so there shouldn't be
|
||||
# any slashes in it which could change the request path.
|
||||
safe="",
|
||||
encoding="utf8",
|
||||
)
|
||||
|
||||
resultant_url = urljoin(
|
||||
# We have to add a trailing slash to the base URL to ensure that the
|
||||
# last path segment is not stripped away when joining with another path.
|
||||
f"{base_url}/",
|
||||
f"{idp_id}?{serialized_query_parameters}",
|
||||
f"{url_encoded_idp_id}?{serialized_query_parameters}",
|
||||
)
|
||||
else:
|
||||
resultant_url = f"{base_url}?{serialized_query_parameters}"
|
||||
|
||||
@ -63,6 +63,22 @@ class PickIdpResource(DirectServeHtmlResource):
|
||||
if not idp:
|
||||
return await self._serve_id_picker(request, client_redirect_url)
|
||||
|
||||
# Validate the `idp` query parameter. We should only be working with known IdPs.
|
||||
# No need waste further effort if we don't know about it.
|
||||
#
|
||||
# Although, we primarily prevent open redirect attacks by URL encoding all of
|
||||
# the parameters we use in the redirect URL below, this validation also helps
|
||||
# prevent Synapse from crafting arbitrary URLs and being used in open redirect
|
||||
# attacks (defense in depth).
|
||||
providers = self._sso_handler.get_identity_providers()
|
||||
auth_provider = providers.get(idp)
|
||||
if not auth_provider:
|
||||
logger.info("Unknown idp %r", idp)
|
||||
self._sso_handler.render_error(
|
||||
request, "unknown_idp", "Unknown identity provider ID"
|
||||
)
|
||||
return
|
||||
|
||||
# Otherwise, redirect to the login SSO redirect endpoint for the given IdP
|
||||
# (which will in turn take us to the the IdP's redirect URI).
|
||||
#
|
||||
|
||||
@ -53,3 +53,29 @@ class LoginSSORedirectURIBuilderTestCase(HomeserverTestCase):
|
||||
),
|
||||
"https://test/_matrix/client/v3/login/sso/redirect/oidc-github?redirectUrl=https%3A%2F%2Fx%3F%3Cab+c%3E%26q%22%2B%253D%252B%22%3D%22f%C3%B6%2526%3Do%22",
|
||||
)
|
||||
|
||||
def test_idp_id_with_slash_is_escaped(self) -> None:
|
||||
"""
|
||||
Test to make sure that we properly URL encode the IdP ID.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.login_sso_redirect_url_builder.build_login_sso_redirect_uri(
|
||||
idp_id="foo/bar",
|
||||
client_redirect_url="http://example.com/redirect",
|
||||
),
|
||||
"https://test/_matrix/client/v3/login/sso/redirect/foo%2Fbar?redirectUrl=http%3A%2F%2Fexample.com%2Fredirect",
|
||||
)
|
||||
|
||||
def test_url_as_idp_id_is_escaped(self) -> None:
|
||||
"""
|
||||
Test to make sure that we properly URL encode the IdP ID.
|
||||
|
||||
The IdP ID shouldn't be a URL.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.login_sso_redirect_url_builder.build_login_sso_redirect_uri(
|
||||
idp_id="http://should-not-be-url.com/",
|
||||
client_redirect_url="http://example.com/redirect",
|
||||
),
|
||||
"https://test/_matrix/client/v3/login/sso/redirect/http%3A%2F%2Fshould-not-be-url.com%2F?redirectUrl=http%3A%2F%2Fexample.com%2Fredirect",
|
||||
)
|
||||
|
||||
@ -939,39 +939,32 @@ class MultiSSOTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual(chan.code, 200, chan.result)
|
||||
self.assertEqual(chan.json_body["user_id"], "@user1:test")
|
||||
|
||||
def test_multi_sso_redirect_to_unknown(self) -> None:
|
||||
"""An unknown IdP should cause a 404"""
|
||||
def test_multi_sso_redirect_unknown_idp(self) -> None:
|
||||
"""An unknown IdP should cause a 400 bad request error"""
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/client/pick_idp?redirectUrl=http://x&idp=xyz",
|
||||
)
|
||||
self.assertEqual(channel.code, 302, channel.result)
|
||||
location_headers = channel.headers.getRawHeaders("Location")
|
||||
assert location_headers
|
||||
sso_login_redirect_uri = location_headers[0]
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
|
||||
# it should redirect us to the standard login SSO redirect flow
|
||||
self.assertEqual(
|
||||
sso_login_redirect_uri,
|
||||
self.login_sso_redirect_url_builder.build_login_sso_redirect_uri(
|
||||
idp_id="xyz", client_redirect_url="http://x"
|
||||
),
|
||||
)
|
||||
def test_multi_sso_redirect_unknown_idp_as_url(self) -> None:
|
||||
"""
|
||||
An unknown IdP that looks like a URL should cause a 400 bad request error (to
|
||||
avoid open redirects).
|
||||
|
||||
# follow the redirect
|
||||
Ideally, we'd have another test for a known IdP with a URL as the `idp_id`, but
|
||||
we can't configure that in our tests because the config validation on
|
||||
`oidc_providers` only allows a subset of characters. If we could configure
|
||||
`oidc_providers` with a URL as the `idp_id`, it should still be URL-encoded
|
||||
properly to avoid open redirections. We do have `test_url_as_idp_id_is_escaped`
|
||||
in the URL building tests to cover this case but is only a unit test vs
|
||||
something at the REST layer here that covers things end-to-end.
|
||||
"""
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
# We have to make this relative to be compatible with `make_request(...)`
|
||||
get_relative_uri_from_absolute_uri(sso_login_redirect_uri),
|
||||
# We have to set the Host header to match the `public_baseurl` to avoid
|
||||
# the extra redirect in the `SsoRedirectServlet` in order for the
|
||||
# cookies to be visible.
|
||||
custom_headers=[
|
||||
("Host", SYNAPSE_SERVER_PUBLIC_HOSTNAME),
|
||||
],
|
||||
"/_synapse/client/pick_idp?redirectUrl=something&idp=https://element.io/",
|
||||
)
|
||||
|
||||
self.assertEqual(channel.code, 404, channel.result)
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
|
||||
def test_client_idp_redirect_to_unknown(self) -> None:
|
||||
"""If the client tries to pick an unknown IdP, return a 404"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user