{"uuid": "862e95b4-6bb7-417f-8e2a-c5b59e279a5d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-51584", "type": "seen", "source": "https://gist.github.com/seiyaibuki0523/33a4c06eb7d10914e5e5152ecd5100ec", "content": "# CVE-2026-51584 \u2014 SSO Account Takeover in Memos (Missing External-Identity Binding)\n\n&gt; Source-level security advisory. Memos' OAuth2/SSO sign-in resolved the local\n&gt; account using the IdP-supplied identifier (username) as the sole lookup key,\n&gt; with no binding to the IdP's stable subject. An attacker who controls their\n&gt; identifier on any configured public IdP could take over an arbitrary local\n&gt; account.\n\n## Summary\n\n| Field | Value |\n|---|---|\n| CVE | `CVE-2026-51584` |\n| Vulnerability type | Incorrect Access Control \u2192 Account Takeover (CWE-287 / CWE-284) |\n| Vendor / product | usememos / memos |\n| Affected component | `server/router/api/v1/auth_service.go` (`SignIn`, ssoCredentials branch) |\n| Affected versions | \u2264 v0.27.1 |\n| Fixed version | v0.28.0 |\n| Fixing commit | [`d688914b`](https://github.com/usememos/memos/commit/d688914b2864791eeadbf21c882608632875f17c) |\n| Attack type | Remote |\n| Impact | Escalation of privileges / account takeover |\n| Discoverer | Casper Chen, Cevanex |\n\n## Severity\n\n- **Severity:** High\n- **CVSS v3.1 (suggested):** `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N` \u2192 Base 9.1 *(inferred; not yet NVD-assigned)*\n- **Source:** inferred from source-level analysis, not NVD\n\nWhere SSO is enabled against a public IdP whose mapped identifier is\nattacker-controllable (username, email, preferred_username, or name), an\nattacker can authenticate as any existing local user, including admins \u2014\na full account takeover with no prior access to the victim account.\n\n## Root Cause\n\nIn the vulnerable `SignIn` handler, the SSO branch looked up the local user\nusing only the IdP-supplied identifier as the key:\n\n```go\n// server/router/api/v1/auth_service.go (v0.27.x, ssoCredentials branch)\nuser, err := s.Store.GetUser(ctx, &amp;store.FindUser{\n    Username: &amp;userInfo.Identifier,   // &lt;-- only lookup key\n})\nif err != nil { /* ... */ }\nif user == nil {\n    // ... create new user with Username: userInfo.Identifier\n}\nexistingUser = user\n// existingUser is then passed to doSignIn(), which signs a JWT containing\n// existingUser.ID, existingUser.Username, existingUser.Role.\n```\n\nThere was **no** `user_identity` table or equivalent `(user_id, idp_id,\nexternal_sub)` binding. The IdP's stable `sub` claim was discarded \u2014\n`oauth2.IdentityProvider.UserInfo` populated only `Identifier` / `DisplayName`\n/ `Email` / `AvatarURL` from the configured `field_mapping`. By default and in\nevery documented example, `field_mapping.identifier` is `username`, `email`,\n`preferred_username`, or `name` \u2014 all attacker-controllable on any major public\nIdP.\n\nConsequently, an attacker who sets their identifier on the IdP to match a\nvictim's Memos username causes the lookup to resolve to the victim's local\naccount, and `doSignIn()` mints a valid session (JWT) for that account.\n\n## Proof of Concept\n\nPrerequisites: a Memos instance with OAuth2 SSO enabled against a public IdP,\nusing a default `field_mapping.identifier` (e.g. `email` or\n`preferred_username`), and knowledge of a victim's Memos username.\n\n1. On the configured IdP, set the account's mapped identifier field to the\n   victim's Memos username / email.\n2. Complete the standard OAuth2 authorization-code flow into Memos.\n3. During `SignIn`, `GetUser(FindUser{Username: &amp;userInfo.Identifier})` resolves\n   to the **victim's** existing local user.\n4. `doSignIn()` issues an access token bound to the victim's ID / username /\n   role. The attacker now holds a valid session for the victim account.\n\n## The Fix\n\nFixed in **v0.28.0** by commit\n[`d688914b`](https://github.com/usememos/memos/commit/d688914b2864791eeadbf21c882608632875f17c)\n(`feat(auth): add SSO user identity linkage (#5883)`, boojack, 2026-04-23).\n\nThe fix removes the identifier-as-lookup-key branch and introduces an\nexternal-identity linkage:\n\n- New `user_identity` table via migration\n  `store/migration/{sqlite,mysql,postgres}/0.28/00__user_identity.sql`, with a\n  `(provider_uid, extern_uid)` unique constraint.\n- SSO lookup now goes through `resolveSSOUser()`, which resolves the local user\n  via the linkage table instead of `userInfo.Identifier`.\n- On the miss path, a local user is created with a UUID-based username derived\n  by `deriveSSOUsername()`, so the IdP identifier is no longer usable as a local\n  username key; the `(provider, extern_uid)` linkage is committed atomically\n  with the user.\n\nThe post-fix `SignIn` delegates to the helper:\n\n```go\n} else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil {\n    identityProvider, userInfo, err := s.resolveSSOIdentity(ctx, ssoCredentials.IdpName, ssoCredentials.Code, ssoCredentials.RedirectUri, ssoCredentials.CodeVerifier)\n    if err != nil { return nil, err }\n    user, err := s.resolveSSOUser(ctx, nil, identityProvider, userInfo)\n    if err != nil { return nil, err }\n    existingUser = user\n}\n```\n\n**Follow-up hardening (additional reference):**\n[`019f4f9a`](https://github.com/usememos/memos/commit/019f4f9adcfcfca73fc9e2a966d0569fac888a2c)\n(`fix(auth): provision SSO users atomically (#6114)`, shipped in v0.30.0) closes\na race in the linkage insert on concurrent first logins.\n\n## Remediation for Operators\n\nUpgrade to **v0.28.0 or later** (v0.30.0+ recommended, to include the atomic\nprovisioning fix). After upgrading, audit existing SSO-linked accounts.\n\n## Timeline\n\n- 2026-04-23 \u2014 fix `d688914b` merged (#5883), released in v0.28.0\n- v0.30.0 \u2014 follow-up hardening `019f4f9a` (#6114)\n- 2026-07-23 \u2014 CVE-2026-51584 reserved by MITRE\n\n## References\n\n- https://github.com/usememos/memos\n- https://github.com/usememos/memos/commit/d688914b2864791eeadbf21c882608632875f17c\n- https://github.com/usememos/memos/commit/019f4f9adcfcfca73fc9e2a966d0569fac888a2c\n", "creation_timestamp": "2026-08-11T04:57:17.905405Z"}