fix(security): OAuth account-takeover via adoption-bypass, login timing oracle, SiteName HTML injection#2929
Open
jaxxjj wants to merge 1 commit into
Open
Conversation
…ose login timing oracle; escape SiteName in <title> Addresses three issues found in a security review: - CRITICAL (S2A-001): OAuth pending-completion exchange bound an attacker's OAuth identity to a pre-existing local account matched by (unverified) email without a password check, because including any adoption field in the request body bypassed the choice-screen guard. Add an explicit existing_account_bindable guard in ExchangePendingOAuthCompletion: while a session is in the account-choice/binding state, record any adoption decision but never fall through to applyPendingOAuthAdoption — binding still requires /oauth/<provider>/bind-login with password proof. Adds a regression test. - LOW (S2A-016): password login ran bcrypt only for existing accounts, leaking account existence via response timing. Run a dummy bcrypt compare on the user-not-found path to equalize work. - LOW (S2A-019): admin-set SiteName was concatenated into the server-rendered <title> without escaping (stored HTML injection). Escape with html.EscapeString.
Contributor
|
Thank you for your contribution! Before we can merge this PR, we need you to sign our Contributor License Agreement (CLA). To sign, please reply with the following comment:
You only need to sign once — it will be valid for all your future contributions to this project. I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
|
I have read the CLA Document and I hereby sign the CLA |
|
recheck |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes three security issues found during a code review of the gateway. Scoped to small, low-risk changes only — no schema migrations or default-behavior changes.
🔴 Critical — OAuth account takeover via adoption-bypass (auth pending flow)
When a third-party login matches an existing local account by email, the flow is supposed to require the user to prove ownership via
/oauth/<provider>/bind-login(password) before the OAuth identity is linked. ButExchangePendingOAuthCompletiononly showed that choice/bind screen when the request body carried no adoption decision (if !adoptionDecision.hasDecision()). Including any adoption field (e.g.{"adopt_avatar":false}) bypassed the guard and fell through toapplyPendingOAuthAdoption(..., session.TargetUserID), binding a brand-new OAuth identity to the email-matched account with no password check. On the next login the attacker received a full token pair for the victim.With a permissive IdP (or LinuxDo, which has no
email_verified) and the defaultOIDCConnectRequireEmailVerified=false, an attacker who knows a victim's email (e.g. an admin's) could take over the account.Fix: add an explicit
existing_account_bindableguard in the exchange handler. While a session is in the account-choice/binding state, any adoption decision is recorded but the handler never binds or consumes the session — binding still requires the password bind-login path. Theexisting_account_bindableflag is set only on the bind-to-existing-account path (pendingOAuthChoiceCompletionResponse), never on new-account adoption, so legitimate flows (new-account profile adoption, existing-identity login,bind_current_user, password bind-login) are unaffected. Includes a regression test that fires the exact bypass payload and asserts no identity is bound and the session is not consumed.🟡 Low — login timing oracle / username enumeration (
AuthService.Login)Loginreturned immediately on user-not-found but ran a full bcrypt compare (tens of ms) for existing accounts, so response latency revealed whether an email is registered. Run a dummy bcrypt compare against a fixed hash on the not-found path to equalize work. (Forgot-password was already hardened this way; login was not.)🟡 Low — stored HTML injection via
SiteNamein<title>The admin-set
SiteNamewas concatenated into the server-rendered<title>with no escaping. CSP blocks script execution, but HTML/UI injection (content spoofing, phishing overlay) was possible and served to all visitors. Escape withhtml.EscapeString.Test plan
go build ./internal/handler/... ./internal/service/...— passesTestExchangePendingOAuthCompletionChoiceStateRejectsAdoptionDrivenBindingcovers the critical fix (fails before the patch, passes after)gofmtclean on all changed filesNot included (flagged for maintainers)
These came up in the same review but need design/migration decisions, so they're intentionally out of scope here:
security.url_allowlist.*) and the first-run setup wizard defaulting to0.0.0.0— fixing safely means changing defaults/UX