COSCUP 2026

同個 Email、兩種登入方式 Google 登入整合的
四個帳號接管路徑

週六下午,你明明有很多選擇 但你選擇來參加 COSCUP

先為願意在假日走進來學習的自己與大家,掌聲鼓勵!

02 / 39
BEFORE WE START

開場提醒

共筆文件
跟上筆記、掌握進度
會後簡報會放在這裡
有問題也可以直接打在這裡
也歡迎大家一起共筆
03 / 39
Gui 桂

Gui

軟體工程師

我是 Gui 桂,目前是一名網頁開發者。從高職就讀資處科開始到後來進入職場,深知這個領域需要不斷追求卓越、終身成長,持續往技能更加全面的開發者邁進。

同時不斷參與社群,參與研討會志工與讀書會,也即將擔任網頁課程老師,希望能將過去所學透過社群分享給更多人。

04 / 39
ABOUT ME, TOO

我平常的開發模式

BDDBehavior-Driven Development?

才不是

BDDBeer-Driven Development

不管是寫 code,還是準備這場 COSCUP 分享,卡關的時候先喝一杯,思路常常就順了。

05 / 39
SECTION
1

我們的會員系統,一開始
都是怎麼接上 Google 登入的

06 / 39
SECTION 1

先看使用者眼中看到的畫面

1
點下登入 使用者點「使用 Google 登入」
2
同意授權 Google 跳出畫面,使用者選帳號、確認授權
3
完成登入 自動跳回平台,已經是登入狀態
07 / 39
SECTION 1

Google 第三方登入是怎麼跑一輪的

使用者
我們的網站
Google
1點下「使用 Google 登入」,跳去 Google 同意授權
2Google 核發一份 JWT 格式的身分憑證,附上 email、sub(主體識別碼)
3後端讀出憑證,比對或建立會員、完成登入
08 / 39
SECTION 1

名詞解釋:Provider 與 Sub

provider身分提供者

替你做身分驗證的第三方服務,例如 Google、Facebook、Apple、Microsoft、LINE。走這條登入路徑時,帳密驗證的工作都在 provider 那邊做完,預設不用自己存密碼、比對密碼,只要驗證它簽發的 JWT 是不是真的。

sub主體識別碼

provider 給你的其實是一組 UID,Google 認人靠這個,不是 email。sub 像身分證字號不會變,email 像聯絡資料,隨時會換。這次分享的系統,一開始用的就是 email。

09 / 39
SECTION 1

還有一個名詞:平台帳號

平台帳號跟 provider 相對的另一條路

使用者直接在我們自己的系統設帳號、密碼登入,不透過任何 provider。今天講的「帳密驗證」「密碼」,指的都是這一種帳號;跟走 Google 登入是兩條平行的路,同一個人可能兩條路都走過。

10 / 39
SECTION 1

這一步用 Email 找會員,問題就從這裡開始

從 provider 拿到使用者的 emailsubject identifiersub

用 email 去 DB 找會員,找到就直接登入、找不到就建立新會員

11 / 39
SECTION 1

你的網站,符合以下任何一個條件嗎?

1
同時支援「平台帳號註冊」跟「第三方登入」
2
允許會員自己刪除帳號
3
Email 可能已被別人在 Google 端搶先註冊(例如離職員工的舊信箱)
12 / 39
SECTION
2

接下來我們可以看一下
真實發生過的案例

13 / 39
SECTION 2

買下一個舊網域,就能登入別人的系統?

一間公司倒閉,網域被別人買走,買家只要照舊員工的信箱格式建新帳號,就能用 Google 登入這間公司以前用過的 Slack、Zoom、ChatGPT,甚至 HR 系統。問題不是 Google 沒有不變的識別碼,sub 就是那個不變的識別碼;問題是這些被入侵的服務一開始沒拿 sub 當識別,用的是網域轉手就會重複出現的 email。

來源:Truffle Security 原文 · SecurityWeek
14 / 39
SECTION 2

近兩年,剛好對上主題的兩個 CVE(公開漏洞編號)

CVE-2025-24856(搶先卡位)
TYPO3 的第三方登入套件

攻擊者搶先用你的 email 註冊,就能卡在你前面

CVE-2024-56329(未確認綁定)
Socialstream

綁定 Google 帳號時,沒跳出確認畫面就直接綁上去了

15 / 39
SECTION 2

某訂房網也曾被這樣入侵過

某訂房網的 Google/Facebook 登入流程裡,有好幾個環節沒有再次確認授權是不是使用者本人要的。攻擊者串起這幾個環節,把授權過程中的憑證半路攔截走,直接拿到受害者的帳號、訂房紀錄,甚至能取消或代訂行程。這類是 OAuth 流程實作層的漏洞,跟今天要講的 email 比對是不同層,但同樣說明:整合細節一出錯,代價都是帳號。

來源:Salt Labs 研究報告
16 / 39
SECTION 2

網域轉手、自動接管、殘留綁定、搶先卡位,四個路徑說到底都是同一個問題:拿 email 當身分,卻沒想清楚所有狀況

網域轉手的真實案例剛剛看過了,怎麼防、留到最後收尾;剩下三個,接下來照著我們自己系統的修補,一段一段走。

17 / 39
SECTION
3

我們怎麼擋下 Google
自動接管平台帳號

18 / 39
SECTION 3

Jack 只需要同一個 Email,就能拿到 JWT

前提:Jack 登入得了這個信箱的 Google 帳號,例如離職員工的公司信箱被回收、再發給新人

Amy
Jack
後端
1用 amy@example.com 註冊、設密碼
2同一個 email 走 Google 登入
3用 email 找到 Amy,簽出平台自己的 JWT
4拿 JWT 登入 Amy 帳號
19 / 39
SECTION 3

只要平台帳號已經設過密碼,我們就拒絕 Google 自動登入接管。使用者必須先用密碼登入,登入之後,才能在會員頁面主動發起綁定。

原因是 provider 驗證的是「你能登入這個 Google 帳號」,不是「你是這個平台帳號的擁有者」。前面 Socialstream 的 CVE 缺的就是這一步:綁定沒經過本人確認。讓綁定只能由密碼登入後的使用者主動發起,這個動作本身就是確認。

20 / 39
SECTION 3

偵測到帳號已有密碼,就擋下 Google 登入

member.controller.ts
if (existing_member) {
  // 只要帳號設過密碼,不管當初怎麼註冊的,一律擋下 Google 自動登入
  if (existing_member.password) {
    response.status(409).json({
      status: "account_exists",
      message: "請先用密碼登入後,至會員頁面綁定 Google 帳號。",
    });
    return;
  }
}
409 表示帳號衝突,跟登入驗證失敗的 401 是不同狀況;前端可以藉此準確導向綁定流程,不會誤判成登入錯誤。
21 / 39
SECTION 3

實作這段邏輯時,有幾個地方要注意

1
用不同的錯誤代碼區分「帳號已存在」跟「登入失敗」,畫面才能準確帶使用者到下一步,不會顯示錯的提示。
2
只看 login_methods 不夠準,Google 使用者事後也可能補設密碼(怎麼補,下一章會講),這時清單裡還是有 Google,真正該檢查的是有沒有密碼。
22 / 39
SECTION
4

解綁、刪帳號之前,
使用者得先有密碼

23 / 39
SECTION 4

Google 登入的使用者,平台帳號原本就沒有密碼

沒有密碼,就沒辦法解綁、也沒辦法安全刪除帳號。所以第一步,要先讓 Google 使用者能自己設定密碼。

24 / 39
SECTION 4
change_password — member.controller.ts
// Google 使用者(無密碼):跳過舊密碼驗證,視為「設定密碼」
const is_google_user_without_password =
  member.login_methods.includes('google') && !member.password;

if (!is_google_user_without_password) {
  // 平台使用者:仍然需要驗舊密碼
  if (!old_password) {
    return response.status(400).json({
      status: "old_password_required",
    });
  }
}

// ……以下接著比對舊密碼是否正確、寫入新密碼(略)
判斷式只看兩件事:是 Google 登入、而且密碼欄位是空的。符合才允許跳過舊密碼驗證。前提是後端本來就有把 Google JWT 驗證做對,確認它沒被偽造、也確實是核發給你的應用程式。
25 / 39
SECTION 4

刪除帳號沒處理綁定,會留下什麼

1
Amy 用 Google 註冊平台帳號,後來按下「刪除帳號」
2
平台執行軟刪除,但沒處理 Google 綁定關聯
3
三個月後,這個 email 換手到 Jack 手上(例如公司信箱回收),他拿它走 Google 登入
4
軟刪除只是標記停用,資料還在;只要有一段查詢漏了檢查這個標記,Jack 就能直接拿到 Amy 的歷史資料
26 / 39
SECTION 4

拆成兩步:先解綁,才能刪帳號

1
解綁,但解綁前要先有平台密碼,不然會把自己鎖在系統外
2
刪帳號,這時 login_methods 裡已經沒有 Google,可以安全執行軟刪除
27 / 39
SECTION 4
delete_account — member.controller.ts
// 有第三方登入綁定,要先解綁才能刪除
if (member.login_methods.includes('google')) {
  response.status(400).json({
    status: "google_bound",
    message: "請先解除 Google 綁定後再刪除帳號。",
  });
  return;
}

member.is_disabled = true;
await member.save();
有第三方登入綁定,要先解綁才能刪除,避免刪除後留下沒人管的 Google 綁定關聯
28 / 39
SECTION 4
unbind_google_account — 同檔案
// 沒有密碼不能解綁,避免把使用者鎖在系統外
if (!current_member.password
    || current_member.password.trim() === '') {
  response.status(400).json({
    status: "error",
    message: "請先設定密碼,或聯絡客服協助處理。",
  });
  return;
}

await Member.findByIdAndUpdate(current_member._id, {
  $pull: { login_methods: 'google' },
});
沒有密碼不能解綁,這跟前面設定密碼那一步是配對的。
29 / 39
SECTION 4

設定密碼、解綁、刪帳號,是同一套流程

少了設定密碼,Google 使用者解綁後就沒有密碼可以登入,會把自己鎖在系統外。這幾個改動,連同前面擋下 Google 自動接管平台帳號的修補,是同一波修補、一起上線。

30 / 39
SECTION
5

Pre-account Hijacking:
當帳號還沒建立就先被別人卡位

31 / 39
SECTION 5

帳號還沒建立,就已經能被卡位

1
Jack 知道(或猜到) Amy 會用 amy@company.com 註冊某個服務
2
Jack 搶先註冊平台帳號,但他根本沒有那個信箱的存取權
3
Amy 後來真的來註冊,發現「Email 已存在」,於是改用 Google 登入
4
系統把 Google 登入自動關聯到那個未驗證的 Jack 帳號, Amy 進到的其實是 Jack 預埋的帳號
32 / 39
SECTION 5

未驗證的帳號,不該有任何權限

不能留設定、不能收通知,更不能被拿去接管,這也代表 email 驗證要在註冊當下就同步阻擋,不能先讓帳號能用、之後才補驗證。Jack 那個從沒驗證過 email 的預埋帳號,卻已經能被 Google 登入自動關聯,就是這件事沒做好的後果。

33 / 39
SECTION 5

接管未驗證帳號,要重新驗證 Email 所有權

Amy 發現「Email 已存在」時,不能直接用 Google 登入接手那個帳號;系統得先寄一封驗證信,等她點過信裡的連結、確認她真的擁有這個信箱,才能把帳號交給她。

34 / 39
SECTION 5

Email 換手後,舊帳號 session 全部失效

失效處理(示意)
// 確認 email 已換手(重新驗證所有權通過)之後
await Session.deleteMany({ member_id: old_member._id });
await RefreshToken.deleteMany({ member_id: old_member._id });
就算前面都做對,只要舊的登入狀態還留著,換手的人一樣能沿用舊 session 進去。作廢的是伺服器端 session 跟長效的 refresh token;短效的 JWT 簽出去就收不回來,讓它自然過期,但拿不到新的 token,就進不來了。
35 / 39
SECTION 5

那能不能直接換成 Sub?

還記得網域轉手的案例嗎?email 會跟著網域一起換手,sub 不會。那能不能把比對鍵換成 sub?

1
平台帳號從一開始就沒有走 provider 登入,根本沒有 sub 可以存
2
只走過 Google 登入的舊帳號,也要等使用者下次登入才補得到 sub
3
換鍵期間新舊比對邏輯得並存,本身就是一次資料遷移工程,不是一個小修補
36 / 39
SECTION 5

如果你的會員系統還沒動工

第一天就用 sub 當識別,email 只當聯絡資料。

剛剛那三個包袱,全部是系統已經上線才有的遷移成本。新系統從第一天就拿 provider 加 sub 當比對鍵,再把 email 所有權驗證做好,今天講的四個路徑,大多從源頭就不成立。

37 / 39
RECAP

四個路徑,四套防禦

1
網域轉手:email 會跟著網域換手,換手後舊 session 全部失效,比對鍵長期朝 sub 靠攏
2
自動接管:帳號已有密碼就擋下 Google 自動登入,先用密碼登入、再主動發起綁定
3
殘留綁定:先解綁才能刪帳號,解綁前要先讓使用者設定密碼
4
搶先卡位:未驗證帳號不給權限,接管前重新驗證 email 所有權
38 / 39
COSCUP 2026

謝謝聆聽 Q&A

共筆文件
此 Gui 非彼 GUI
Gui Blog - 網站的工具人