From a401bb21d681c5a974352a6548e065103ed9c04a Mon Sep 17 00:00:00 2001 From: tdurieux Date: Sun, 6 Sep 2026 09:16:53 +0200 Subject: [PATCH] fix: reject OAuth linking across different GitHub identities --- src/server/routes/connection.ts | 4 ++++ test/production-regressions.test.js | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/server/routes/connection.ts b/src/server/routes/connection.ts index 80e2e6c..17694b4 100644 --- a/src/server/routes/connection.ts +++ b/src/server/routes/connection.ts @@ -69,6 +69,10 @@ const verify = async ( // the isAdmin flag. user = await UserModel.findOne({ username: profile.username }); if (user) { + if (user.externalIDs?.github && user.externalIDs.github !== profile.id) { + done(new AnonymousError("not_connected", { httpStatus: 401 })); + return; + } if (isDisabledAccount(user.status)) { done( new AnonymousError( diff --git a/test/production-regressions.test.js b/test/production-regressions.test.js index 897ae37..3d369f7 100644 --- a/test/production-regressions.test.js +++ b/test/production-regressions.test.js @@ -54,6 +54,17 @@ describe("production regressions", function () { expect((await error)[0].message).to.include("exceeded"); expect(emitted).to.equal(false); }); + + it("does not link a recycled OAuth username to an existing GitHub identity", async function () { + require("../src/server/routes/connection"); + const passport = require("passport"); + const UserModel = require("../src/core/model/users/users.model").default; + let calls = 0; + stub(UserModel, "findOne", async () => ++calls === 1 ? null : { externalIDs: { github: "old-id" }, isAdmin: true }); + stub(UserModel, "updateOne", () => { throw new Error("must not overwrite identity"); }); + const error = await new Promise(resolve => passport._strategy("github")._verify("token", "", { id: "new-id", username: "recycled" }, resolve)); + expect(error.message).to.equal("not_connected"); + }); it("omits an upstream length when later text is rewritten", async function () { const File = require("../src/core/AnonymizedFile").default; stub(config, "STREAMER_ENTRYPOINT", "");