Open micolous opened 9 hours ago
Thanks for the very detailed report! This is a well-known behaviour and intended to work like this, as the main usage we've seen of this feature is for deployments moving off a password-based auth to an SSO-based one. In those cases, it would be impractical to ask the user to re-enter their original credentials, as the intent is to make it as smooth as possible for end users.
This is what the docs says about the option:
Two things to note:
Which makes me feel like a sensible homeserver administrator would not enable this with a public IDP, but maybe with one they control.
I'm happy with expanding this particular point in the doc to warn about the potential misuse of this option, but would rather avoid changing any of the existing behaviour. Feel free to open a PR to help improve this
Last but not least, Synapse's auth is being reworked through Matrix Authentication Service, which is quite stricter on this, and currently doesn't have this 'allow existing users' feature. We plan to add it, with a more granular behaviour, so if you have thoughts on the implementation, please voice them in the following issue: https://github.com/element-hq/matrix-authentication-service/issues/2089
The
oidc_providers.allow_existing_users
documentation is pretty sparse:The way the sample configurations do this makes assumptions contrary to OIDC claim stability guidelines. If a Synapse instance was configured in this way, it could allow account take-overs[^1] by an attacker with an unknown-to-Synapse IdP account.
The documentation should describe the security implications of enabling the option.
While there are some weaknesses in Synapse that make this possible at all, the ability to exploit it is mostly configuration dependant – so I feel this is a "security-related docs bug" rather than "docs-related security bug".
[^1]: If a user has enabled end-to-end encryption and uses it in all channels, I suspect it would prevent an attacker from accessing prior messages or impersonating the user on those channels.
Background
From OpenID Connect Basic Client Implementer's Guide 1.0, Section 2.5.3, Claim Stability and Uniqueness, emphasis added:
By default, Synapse uses the
sub
claim to uniquely identify a user, which is in-line with OIDC recommendations.Detail
Setting
oidc_providers.allow_existing_users: true
triggers an extra code path ingrandfather_existing_users()
on login if lookup by external user ID fails.This maps an OIDC user to a Matrix user based on
localpart_template
:https://github.com/element-hq/synapse/blob/2ce7a1edf7291cfda14bb975135eb7c989d910bc/synapse/handlers/oidc.py#L1247-L1255
If there is an existing Matrix user which maps, the new
sub
is added to the database:https://github.com/element-hq/synapse/blob/2ce7a1edf7291cfda14bb975135eb7c989d910bc/synapse/handlers/sso.py#L474-L479
As there doesn't seem to be any further authentication (such as requesting a user login with a password, or with another configured external identity provider), my working assumption (from static analysis) is that enabling
allow_existing_users
allows an unknown-to-Synapse OIDC user to permanently link their OIDC identity to an existing Matrix account that matches bylocalpart_template
.I couldn't find anywhere that branch checks to see if a matched account already has an
user_external_id
associated with it (only that an external ID can't be re-used), so it looks like this would allow two OIDC users to "claim" the same Matrix account.One caveat is that if an instance sets
enable_registration: true
(the default), an attacker would need a new IdP account for every attempt they made to take over an account.The
localpart_template
The impact of the issue has depends on the
localpart_template
.There is no default
localpart_template
, but the example OIDC configs doc have some suggestions, and many of them look insecure:Entra ID (Azure AD): Using everything before an
@
character inpreferred_username
.Entra ID sets the
preferred_username
to the account email address, so this would treatuser@example.com
anduser@example.net
as the same user.Entra ID also has a tenancy ID attribute, but this doesn't seem to be used.
Django OAuth Toolkit: Using everything before an
@
character inemail
.Similar issue to Entra ID.
Shibboleth: Using everything before an
@
character inuser.sub
, and also setsallow_existing_users: true
.Shibboleth has custom mapping rules for
sub
, but this example seems to suggestsub
is anemail
-like field.If it is an email field, then this would have a similar issue to Entra ID, with an insecure-by-default config.
Auth0, Authentik, Keycloak, LemonLDAP: Using
preferred_username
as-is – this is an unstable claim.Twitch: Using
preferred_username
as-is.This is the display name, and can be changed by users, or recycled by Twitch.
GitHub, GitLab, Gitea, Mastodon, Twitter: Using
user.login
/user.nickname
/user.username
as-is.Those accounts may be renamed.
Google: Using
user.given_name
as-is.This may be freely changed by the user.
If an attacker with an unknown-to-Synapse IdP account can discover a Synapse server's
localpart_template
, and find that it is under user control, they could exploit a server withoidc_providers.allow_existing_users: true
:enable_registration: true
(the default), an attacker would need a new IdP account for each takeover attempt, though this is a low bar for a public identity provider.localpart_template
to match the target user.It's not clear to me (from static analysis) whether a user can discover what external user IDs are associated with their account; though it would appear as another unverified session (as they wouldn't have the E2EE keys).
What could change in the docs?
oidc_config.allow_existing_users
should describe how it maps existing users (withlocalpart_template
), and that enabling the option has significant security implications.It should also note that enabling the option adds extra constraints to any claim used by
localpart_template
:matrix_username
) in the IdP to map users, rather than using other, potentially-unstable identifiers (likeemail
orusername
)What could change in Synapse?
When "grandfathering" existing Matrix user accounts, Synapse should also authenticate the user by some other method, even if those methods are not normally available (eg: password backend). This would prevent account take-overs during the migration process.
There should be a way to prevent more than one external user ID being assigned to the same Matrix user. This would prevent account take-overs for accounts which are already migrated to OIDC.