U5 檢查結果:雲端硬碟整合——Google 回呼通知+同步排程主幹

U5 檢查結果:雲端硬碟整合——Google 回呼通知+同步排程主幹

檢查日期 2026-09-25|對應卡片 CM-2156(母卡 CM-2152)|檢查範圍 9 個檔案 1,838 行|工具 run ID wf_32b5fcb9-978

§1

🔴 一句話結論

卡片最擔心的「不登入的回呼入口」是穩的;真正的洞在背景同步:背景工作一律用「可看全部公司」的系統身分在跑,只要有人塞進一個別家公司的專案編號,系統就會照著做。

  • 回呼入口(Google 打進來的那支)擋得住偽造:憑證是 256 位元的隨機碼,錯的一律丟掉;找不到公司、憑證錯、憑證對,回應都一模一樣,外人探不出「這個頻道存不存在」。
  • 🟠 新發現(中風險,三人面板 3:0):任何一家公司的登入帳號,只要知道另一家公司某個專案的編號,就能叫系統把那家的專案結構(專案名、稽核輪次名、控制項名、任務名)抄一份建進自己的雲端硬碟,而且之後往那些資料夾丟檔案,會被當成證據寫進別家公司的稽核任務。門檻是要先拿到對方專案的編號(隨機 UUID,猜不到,要從別處外流)。我在 DEV 用唯讀查詢確認了「讀」這一半。
  • 🟠 新發現(中風險,面板 3:0):系統建的每一個雲端硬碟資料夾(含最上層根資料夾),都被設成「任何拿到連結的人都能編輯」。同一家公司裡任何一個登入帳號都能從系統要到根資料夾的編號,打開就能看、改、刪全公司每個專案的證據;刪掉的檔案會同步回系統,變成證據被刪。這是 FR-016 設計時刻意選的做法,所以要決策者裁定要不要改,不是單純寫錯。
  • 工具另外撿到 4 條「密碼金鑰寫進版本控制」,全部是既有工單(CM-1607/CM-1629/CM-1631)重現,不另計。
  • 我人工另補 3 條低度問題(見總覽 U5-3~U5-5),都不急。

帶著前棒的形狀來看:U1 證實權限地基是穩的。這一棒的兩條中風險都不是地基壞了,而是「沒接上」——背景工作根本沒去用那套地基(改用可看全部的系統身分),手動觸發的入口也刻意不掛專案成員檢查。

§2

這一棒在檢查什麼

客戶可以把系統接上自己公司的 Google 雲端硬碟。接上之後,系統會自動在硬碟裡替每個專案建一整棵資料夾(專案 → 稽核輪次 → 控制項群組 → 控制項 → 評估項目 → 任務),使用者把證據檔丟進任務資料夾,系統就自動抓回來當證據。

這一棒的 9 支程式是這條同步線的「主幹」:

  • Google 回呼通知(2 支):客戶的雲端硬碟一有變動,Google 會主動打我們一支網址通知。這支網址不用登入(打進來的是 Google,不是人),只靠 Google 帶回來的一組「頻道編號+憑證」認身分。
  • 頻道管理(1 支):向 Google 登記「有變動請通知我」,每 7 天要續約一次。
  • 同步排程主幹(5 支):其他功能(建專案、改名、刪任務)要同步雲端時,先在「同步工作表」排一筆工作,背景工人每 5 秒撈一批出來做。
  • Google 連線用戶端(1 支):實際呼叫 Google API 的那一層(建資料夾、分享權限、下載檔案)。

要回答的核心問題是:回呼憑證從哪來、誰能偽造、偽造了能觸發什麼寫入?背景工作用誰的身分跑?

檔案 行數 角色
app/cloud_integration/service/drive_sync_orchestration_service.py 529 同步總指揮:其他功能要排同步工作都呼叫它
infra/cloud_integration/google_drive/google_drive_api_client.py 301 呼叫 Google API(建資料夾、分享、下載)
app/cloud_integration/service/webhook_channel_manager.py 202 向 Google 登記/續約/停止通知頻道
infra/cloud_integration/repository/drive_sync_job_repo_impl.py 191 同步工作表的讀寫
app/cloud_integration/service/project_tree_loader.py 171 讀出一個專案的整棵結構,給建資料夾用
app/cloud_integration/service/drive_sync_worker.py 149 背景工人:撈工作、分派、記成敗
app/cloud_integration/service/google_drive_webhook_service.py 127 驗回呼憑證、排一筆「處理變動」工作
domain/cloud_integration/service/drive_sync_job_domain_service.py 104 排工作、重試間隔、失敗判定
api/cloud_integration/routes/google_drive_webhook_route.py 64 回呼網址入口(不登入)
§3

掃到什麼(總覽)

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 該補檢查的位置 嚴重度+為什麼 來源
U5-1 手動「重建專案資料夾」收任何專案編號,背景工人用可看全部公司的身分去讀 A 公司的人能把 B 公司的專案結構抄進自己的雲端硬碟,還能往 B 公司的稽核任務塞證據 一個有雲端整合授權的登入帳號+自家公司已接雲端硬碟+知道別家公司的專案編號(猜不到,要另外外流) 入口:app/cloud_integration/service/drive_sync_orchestration_service.py:145(排工作前先確認專案屬於這家公司、呼叫者是成員);背景:app/cloud_integration/service/project_tree_loader.py:87(改用工作所屬公司的身分讀,或讀到後比對公司) 🟠 中:跨公司讀+寫,但要先拿到對方的隨機編號 工具 F3,面板 3:0;runner 開檔+DEV 唯讀驗證
U5-2 系統建的每個雲端硬碟資料夾都設成「任何拿到連結的人都能編輯」 拿到任一資料夾連結就能看、改、刪全公司證據,不必登入 Google 也不必登入系統;刪檔會同步成系統內證據被刪 自家公司已接雲端硬碟+拿到任一資料夾連結(公司內任何登入帳號都能從系統要到根資料夾編號) infra/cloud_integration/google_drive/google_drive_api_client.py:294(改成只分享給公司網域或指定帳號);api/cloud_integration/routes/google_drive_integration_route.py:41(根資料夾編號別回給沒有讀取權限的人) 🟠 中:影響大但屬設計選擇,要決策者裁 工具 F4,面板 3:0;runner 開檔核對
U5-3 回呼註解說「會合併重複的待辦工作」,程式裡根本沒有合併 每來一次通知就多排一筆「處理變動」工作,重複的工作都會跑 必須持有正確的頻道憑證(只有 Google 和我們資料庫有) app/cloud_integration/service/google_drive_webhook_service.py:118(排之前先查同公司有沒有還沒跑的同類工作) 🟢 低:外人拿不到憑證;主要是註解宣稱的防線不存在 runner 開檔+實測(未經三人面板)
U5-4 回呼憑證比對用一般字串比對,不是「常數時間比對」 理論上可以量回應時間一個字一個字猜 能打到回呼網址 app/cloud_integration/service/google_drive_webhook_service.py:93-96 🟢 低:憑證 256 位元、網路抖動遠大於比對時間差,實務上猜不出來;改一行就好 runner 開檔(未經三人面板)
U5-5 在雲端硬碟用名字找資料夾時,名字只跳脫單引號、沒跳脫反斜線 專案或任務名以反斜線結尾,可能改寫查詢條件,讓系統「認領」硬碟裡別處的同名資料夾 能改專案/任務名稱的人(專案管理者);自家公司已接雲端硬碟 infra/cloud_integration/google_drive/google_drive_api_client.py:121 🟢 低:只影響自家公司接的那個硬碟;未實測(要真的打 Google API 才知道) runner 開檔(未經三人面板),與總表第 110 項同型
— 密碼金鑰寫進版本控制(4 條) 見下方「工具報的逐條」 能讀程式碼庫 — 中/低 工具 F1/F2/F5/F6,全為既有工單重現,不另計

現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。

  • U5-1(重建資料夾跨公司)=總表第 192 項,✅ 已修(CM-2201,commit 73f5f90af,1.21.0 出貨)。
  • U5-2(資料夾任何人可編輯)=總表第 193 項,🚫 裁定不修(決策者 10-01,分享權限未來由權限管理控管或交客戶自己的 Google 設定)。
  • U5-3(註解說會去重)=總表第 195 項,🚫 裁定不修(同步可重跑、只多跑一次)。
  • U5-4(通行碼非常數時間比對)=總表第 196 項,✅ 已修(CM-2213,commit a5804a990/bde8f9d16,1.21.0 出貨)。
  • U5-5(反斜線未跳脫)=總表第 197 項,✅ 已修(CM-2213,同上 commit,1.21.0 出貨)。
  • 工具 F1/F2/F5/F6 舊帳:原卡 CM-1631/CM-1607/CM-1629 已作廢,實際由 CM-2051(Google 密鑰)、第 10/11 項(金鑰)、CM-2049(資料庫密碼)修掉,均 ✅ 已修。
§4

工具報的逐條

工具一共交回 6 條,三人面板 18 票全投完、零漏投。

F3 → U5-1 🟠 手動重建資料夾可以讀、寫別家公司的專案(中,3:0)

  • 位置:入口 api/cloud_integration/routes/google_drive_sync_route.py:108-121 → 排工作 app/cloud_integration/service/drive_sync_orchestration_service.py:145-157 → 背景讀取 app/cloud_integration/service/project_tree_loader.py:87-91 → 建資料夾 app/cloud_integration/service/handlers/init_project_folders_handler.py(U6 範圍)→ 匯入證據 app/cloud_integration/service/handlers/import_drive_file_handler.py:103(U6 範圍)

  • 白話說明:專案頁有一顆「重建雲端資料夾」按鈕,會打 POST /integrations/google-drive/projects/<專案編號>/init-folders。這支網址只檢查「有登入、公司有買雲端整合」,刻意不檢查你是不是這個專案的成員(程式註解寫明是怕擋到專案管理者)。它把網址上的專案編號原封不動排進同步工作表,掛在呼叫者自己公司名下。 背景工人撈到這筆工作時,整批工作都跑在「系統身分」底下(core/scheduler.py:177 的 system_context),這個身分會關掉資料庫的公司隔離。接著專案樹載入器用「管理員、看全部」的參數查專案(project_tree_loader.py:53-54 寫死 SYSTEM_IS_ADMIN = True),而查專案的那段只用編號比對、不看公司,所以不管那個專案是哪家公司的都查得到。

  • 影響:

    • 讀:B 公司的專案名、每一輪稽核的名稱、控制項群組/控制項/評估項目/任務的名稱,全部變成 A 公司雲端硬碟裡的資料夾名,A 公司的人直接打開就看得到。
    • 寫:系統同時記下「這個資料夾對應 B 公司的某個任務」。A 公司的人往那個資料夾丟檔案,背景工人(一樣是系統身分)會照對應表找到 B 公司的任務,把檔案當成證據寫進去——別家公司的稽核紀錄裡多了一筆來路不明的證據。證據表本身沒有公司欄位、也沒開資料庫隔離(總表第 43 項已記),所以資料庫不會擋。
  • 觸發前提:A 公司有「雲端整合」授權、已接上 Google 雲端硬碟;攻擊者是 A 公司任何一個登入帳號;攻擊者要知道 B 公司某個專案的編號——編號是隨機 UUID 猜不到,要從別的地方外流(例如總表裡其他「任何登入者讀得到別家資料」的洞)。

  • 🔍 runner 開檔核對:屬實。逐支開檔確認:route 只掛 @jwt_required 與 @require_license;drive_sync_admin_service.py:118-124 的註解明寫刻意不加權限;_enqueue_init_project_folders 只檢查「這家公司有沒有接雲端」;infra/flow_control/repository/flow_control_project_repo_impl.py:384-388 只 filter(Project.uid == uid),is_admin=True 時整段可見性檢查跳過。

  • 🔍 DEV 唯讀驗證(2026-09-25 13:09 +08,BEGIN READ ONLY … ROLLBACK,沒有寫入):用本機 DEV 的 cm_app 帳號,拿 131 公司的一個專案,照同一個查詢條件在兩種身分下各查一次:

    • 模擬背景工人的系統身分(app.is_super_admin = t)→ 查得到 1 筆
    • 模擬「工作所屬公司 102」的身分(app.allowed_tenant_paths = /1/102/)→ 0 筆

    → 證實:只要把背景工人換成「以工作所屬公司的身分跑」,資料庫就會自己擋掉。原本想直接呼叫專案樹載入器實跑,但本機缺雲端金鑰、DI 組不起來,改成照同一段 SQL 在資料庫層模擬兩種身分。「寫入別家任務」那一半是讀碼推論,沒有實測(要真的接 Google 硬碟才跑得起來)。

  • 建議修法(兩層都補):

    1. 入口:_enqueue_init_project_folders(或 trigger_init_project_folders)排工作前,先用呼叫者自己的身分(受公司隔離)查一次這個專案;查不到或不是成員就拒絕。建專案、發起覆核這兩條自動路徑的專案編號是伺服器剛建好的,不受影響,但同一支函式守住就一起守住了。
    2. 背景:專案樹載入與證據匯入改成在 tenant_context(job.tenant_id)(只看工作所屬公司的身分)底下跑,或讀到專案/任務後比對它的公司是不是工作的公司。這一層補了,將來再多一個入口漏守也不會出事。

F4 → U5-2 🟠 雲端硬碟資料夾全部「任何拿到連結的人都能編輯」(中,3:0)

  • 位置:infra/cloud_integration/google_drive/google_drive_api_client.py:285-301(share_anyone_writer);呼叫點:根資料夾與各層資料夾 init_project_folders_handler.py:448、任務資料夾 create_folder_handler.py:59、封存資料夾 drive_sync_orchestration_service.py:521 與 archive_drive_file_handler.py:157
  • 白話說明:系統每建一個資料夾,就對它設一條權限「任何人(anyone)、可編輯(writer)」,只加了「搜尋不到」(allowFileDiscovery: false)——意思是不會被搜尋引擎找到,但任何拿到連結的人都能打開、上傳、刪除,不必登入 Google。子資料夾會繼承根資料夾的權限。 而根資料夾的編號,GET /integrations/google-drive 會回給公司內任何登入帳號(google_drive_integration_route.py:31-35 的註解寫明刻意不掛權限,因為專案頁要用它判斷要不要顯示雲端區塊),這支回傳內容裡就有 root_folder_id。
  • 影響:公司內一個沒參與 X 專案的人,要到根資料夾編號、在瀏覽器打開,就能看、下載全公司每個專案的證據;也能刪檔——系統會把刪除同步回來,變成那筆證據在系統裡被刪。連結如果外流(轉寄、瀏覽紀錄、截圖),公司外的人一樣能用。
  • 觸發前提:公司已接雲端硬碟;拿到任一資料夾的連結或編號。
  • 🔍 runner 開檔核對:屬實,權限寫死、沒有任何設定可切換。但要注意這是刻意設計:docs/features/FR-016-2604-google-drive-sync/design.md 第 52 行把「Drive 端細粒度權限」列為不做,統一用「任何拿到連結的人可編輯」。所以這條要回到決策層:當初的取捨(讓使用者不必有公司 Google 帳號也能丟證據)值不值得這個風險。
  • 建議修法:分享對象改成公司的 Google 網域(type: domain)或指定的專案成員帳號(type: user);GET /integrations/google-drive 不回根資料夾編號給沒有 cloud_integration.read 的人(專案頁判斷「有沒有接」只需要連線狀態,用不到編號)。這條要決策者先裁,改了會影響使用者丟證據的流程。

F1/F2/F5/F6 — 密碼金鑰寫進版本控制(全部是既有工單重現,不另計)

工具的「密碼金鑰專掃」順著雲端硬碟的憑證往外追,在 docs/conversation-history/(過去對話紀錄的原文存檔)找到 4 條。範圍內 9 支程式本身沒有任何寫死的密碼金鑰。

工具編號 內容 面板 對應既有 本棒實數
F1 Google 雲端硬碟應用程式密鑰(GOCSPX-…) 中,3:0 CM-1631 git ls-files 實數 12 檔,與 CM-1631 記錄一致
F6 雲端硬碟權杖加密金鑰(DRIVE_TOKEN_ENCRYPTION_KEY) 低(面板從中調降),2:1 CM-1631 反對票認為光有金鑰拿不到密文,要另外拿到資料庫
F2 整份開發用 .env(Anthropic/OpenAI/Google 金鑰) 中(面板從高調降),3:0 總表第 2 項/CM-1607 Anthropic 金鑰 9 檔,與第 2 項記錄一致
F5 同一份 .env 裡的登入簽章金鑰與資料庫/Redis 密碼 中,2:1 CM-1607(簽章金鑰)+CM-1629(資料庫密碼) 反對票指出安裝版每套自己產生簽章金鑰,偽造只對開發機有效
§5

卡片點名的疑點逐條回答

① 回呼憑證比對是不是「常數時間比對」?——🟡 不是,但實務上打不到(→ U5-4)

白話:「常數時間比對」是不管你猜對幾個字,系統回應的快慢都一樣。一般的 != 比對遇到第一個不同的字就停,理論上猜對越多字、回應越慢,量時間就能一字一字試。

google_drive_webhook_service.py:93-96 用的是一般的 !=。但憑證是 secrets.token_urlsafe(32)(webhook_channel_manager.py:113)= 256 位元隨機碼,字元比對的時間差是奈秒級,網路往返的抖動是毫秒級,差了百萬倍,實務上量不出來。仍建議改成 hmac.compare_digest,一行就好,也跟 U3 設定碼的做法一致。

② 找不到頻道時,回應會不會洩露「這個頻道存不存在」?——✅ 不會,不成立

四種情況回應完全相同,一律 HTTP 200、{"received": true}:

情況 系統內部 回給對方
網址上的公司不存在 tenant_context 拋錯 → 記 log 丟掉(:69-75) 200
公司存在但沒接雲端 查不到授權紀錄 → 記 log 丟掉(:87-92) 200
頻道編號或憑證錯 比對失敗 → 記 log 丟掉(:93-104) 200
全對 排一筆工作 200

程式一律回 200 是因為 Google 看到 4xx/5xx 會以為頻道壞了、停止推送(google_drive_webhook_route.py:50-51),順帶也讓外人探不出任何差別。記進 log 的只有公司編號與頻道編號,憑證本身沒進 log;請求標頭的 log 也會把 X-Goog-Channel-Token 遮成 ***(common/middleware/app_mw.py:78-83 的白名單不含它)。

實測(本機直接呼叫驗證邏輯、用假的授權紀錄與假的工作表,不接資料庫):錯憑證 → 0 筆;空憑證 → 0 筆;不存在的公司 → 0 筆;已斷線的公司(授權紀錄的頻道與憑證都是空的)送空標頭 → 0 筆(空字串 "" 與空值 None 比對不相等,擋得住)。

③ 攻擊者能不能狂打回呼,讓系統無限排同步工作?——🟡 外人不行;持有憑證的人可以(→ U5-3)

  • 外人:沒有正確憑證,每一次都在比對那步被丟掉,一筆工作都排不進去。回呼網址本身的成本只是一次資料庫查詢。
  • 持有憑證的人:每打一次就多排一筆。實測:用正確憑證連打 200 次 → 排進 200 筆,沒有任何合併;resource_state 標頭塞 5,000 字的亂碼 → 照樣排進去,只記一筆警告,那 5,000 字原樣存進工作內容。
  • 註解與程式不符:google_drive_webhook_service.py:6 寫「worker 會依 PENDING 狀態去重」,但工作表的寫入、撈取、分派三處(drive_sync_job_repo_impl.py、drive_sync_worker.py)都沒有去重邏輯。另一支處理變動的程式註解承認「兩筆同時跑沒關係,證據檔編號唯一,重複匯入會被資料庫擋掉」——所以重複工作不會寫壞資料,只是浪費。
  • 誰拿得到憑證:只有 Google 與我們資料庫的授權紀錄。所以這條的實際意義是:萬一資料庫外流,或 Google 那邊短時間推大量通知,系統沒有第二道節流。列低。

④ 背景同步用「哪個公司的機器身分」跑?身分從工作紀錄讀,還是從呼叫者帶?——🔴 這是本棒的核心問題(→ U5-1)

  • 公司編號從哪來:每筆工作的公司編號都是伺服器自己決定的——網頁入口一律取自登入身分(user.tenant_id),回呼入口取自網址但要通過憑證比對,工作表本身還有資料庫隔離規則擋「寫入別家公司的工作」(DEV 查證:drive_sync_jobs 的新增規則要求公司在呼叫者可見範圍內)。使用者塞不進一筆「指定別家公司」的工作。✅
  • 但背景工人不用那個公司的身分跑:core/scheduler.py:177 把整批工作包在 system_context(可看全部公司)裡,公司編號只是當作參數傳下去。只要工作內容裡的其他編號(專案、任務)沒有另外核對公司,就會一路讀寫到別家。U5-1 就是這個形狀的具體實例。
  • 其他排工作的入口我逐支追過:
入口 帶進來的編號 有沒有越界風險
建專案、發起覆核(api/project/routes/project_route.py:64、audit_round_route.py:151) 伺服器剛建好的專案 ✅ 無
改專案/稽核輪次/任務名稱(api/flow_control/routes/*) 網址上的編號,但前一步的 service 已驗專案成員 ✅ 無;背景改名時也以公司編號查資料夾對應表
新增任務(job_route.py:208) 評估項目編號 ✅ 無;查資料夾對應表時帶公司編號過濾(drive_sync_orchestration_service.py:348)
刪任務、在流程圖上刪方塊(job_route.py:162、module_frame_item_route.py:118) 任務編號 ✅ 無;在使用者請求內跑(受隔離),查對應表也帶公司編號
手動重建專案資料夾(google_drive_sync_route.py:108) 網址上的專案編號,沒驗 🔴 U5-1
回呼(處理變動) 無外部編號,從 Google 變動清單讀 屬 U6 範圍

⑤ 使用者能不能在工作紀錄塞一筆指定別家公司的工作?——✅ 不能,不成立

見上一條。所有排工作的呼叫都由伺服器帶公司編號,資料庫層還有一道「只能寫自己看得到的公司」的規則兜底。手動重試(manual_retry)只用工作編號查,但它跑在使用者請求內、受資料庫隔離,別家公司的工作查不到(入口屬 U4 範圍,只讀不報)。

⑥ 通知頻道憑證怎麼產生、夠不夠隨機?——✅ 夠,不成立

webhook_channel_manager.py:112-113:頻道編號 secrets.token_urlsafe(16)(128 位元)、憑證 secrets.token_urlsafe(32)(256 位元),用的是 Python 專門做密碼學隨機的 secrets 模組。每次重新登記都換一組新的,舊頻道先停掉。✅

⑦「有人守了一半」共通疑點

疑點樣式 本棒結果
只驗「你是誰」沒驗「這筆是不是你的」 🔴 成立 → U5-1:手動重建入口只驗登入+授權,沒驗專案歸屬
列表有守、單筆沒守 不適用(本棒沒有列表/單筆對照)
route 裝飾器守門但有第二支路由沒掛 ❌ 不成立:回呼只登記一支(api/cloud_integration/__init__.py:79-81)
守門條件用 or 串、其中一個恆真 ❌ 不成立:憑證比對是「頻道錯 或 憑證錯 → 丟掉」,兩個都要對才過
「查不到」跟「沒權限」混成同一個回應 刻意混:回呼四種情況都回 200,這是對的(見②)
背景/系統身分執行時假設呼叫者一定是自己人 🔴 成立 → U5-1:背景工人用可看全部的身分,相信工作內容裡的專案編號
防重放/計次只在單一進程有效 不適用:本棒沒有計次;「去重」連單一進程都沒有(→ U5-3)
註解寫「這裡刻意不檢查」但上一層沒檢查 🔴 成立兩處:drive_sync_admin_service.py:120-124「刻意不掛能力點」但上一層也沒有專案成員檢查(→ U5-1);google_drive_webhook_service.py:6「worker 會去重」但沒有(→ U5-3)
§6

各條詳細(runner 人工補的三條)

以下三條未經三人面板投票,是 runner 自行開檔核對。

U5-3 — 回呼註解宣稱「會去重」,程式裡沒有

  • 嚴重度:🟢 低
  • 位置:app/cloud_integration/service/google_drive_webhook_service.py:6(註解)、:118-123(排工作)
  • 白話說明:Google 每推一次通知,系統就排一筆「處理變動」工作。註解說背景工人會把狀態還在等待中的重複工作合併掉,實際上沒有這段程式。
  • 影響:通知密集時會累積大量重複工作,每一筆都去 Google 拉一次變動清單。重複匯入會被資料庫的唯一約束擋掉,不會寫壞資料,只是浪費 Google 配額與工人時間。
  • 觸發前提:要有正確的頻道憑證(外人沒有);或 Google 本身短時間推大量通知。
  • 實測:本機直接呼叫驗證邏輯,用正確憑證連打 200 次 → 工作表收到 200 筆;resource_state 塞 5,000 字 → 照樣排入、原樣存進工作內容。
  • 建議修法:排之前先查「同一家公司是否已有還沒開始的『處理變動』工作」,有就跳過;或在資料庫加一條「同公司同類型只能有一筆等待中」的部分唯一索引。順手把 resource_state 限定在已知的幾個值、不認得的就不排。

U5-4 — 回呼憑證比對不是常數時間

  • 嚴重度:🟢 低
  • 位置:app/cloud_integration/service/google_drive_webhook_service.py:93-96
  • 白話說明與影響:見疑點①。256 位元的隨機憑證,實務上量時間猜不出來。
  • 觸發前提:能打到回呼網址(對外公開)。
  • 建議修法:hmac.compare_digest(entity.webhook_token or "", channel_token),頻道編號同理。注意授權紀錄的憑證可能是空值,要先轉成空字串,否則會拋錯變成 500(U3-1 同型陷阱)。

U5-5 — 用名字找資料夾時沒跳脫反斜線

  • 嚴重度:🟢 低(未實測)
  • 位置:infra/cloud_integration/google_drive/google_drive_api_client.py:121(find_folders_by_name);呼叫點 init_project_folders_handler.py:428
  • 白話說明:系統建資料夾前,會先在雲端硬碟用名字找「有沒有同名的舊資料夾可以直接認領」。名字塞進 Google 的查詢語法時,只把單引號 ' 換成 \',反斜線 \ 本身沒處理。名字如果以反斜線結尾,例如 abc\,查詢會變成 name='abc\',後面那個引號被當成字面字元,字串一路延伸到下一個引號,查詢條件的結構就被改掉了。我本機組出查詢字串確認了這個結構變化。
  • 影響:名字來自專案、稽核輪次、控制項、任務名稱(專案管理者可以改)。精心設計的名字可能讓「只在這個父資料夾底下找」這個限制失效,讓系統把硬碟裡別處的同名資料夾認領成任務資料夾,之後那個資料夾裡的檔案會被當成證據匯入。影響範圍限在這家公司自己接的那個 Google 帳號的硬碟。
  • 觸發前提:能改專案/任務名稱;公司已接雲端硬碟;要實際打 Google API 才知道 Google 的查詢解析器吃不吃這個結構——本棒沒有接真的硬碟,未實測。
  • 建議修法:先把 \ 換成 \\、再把 ' 換成 \'(順序不能反)。與總表第 110 項(舊證據分類線的同型問題)一起修。

另記一筆(資訊,不列項)

  • google_drive_api_client.py:280:下載檔案時「超過大小上限就中止」的檢查,要等整個分塊下載完才做,而 Google 套件預設一塊是 100 MB。所以單次匯入最多會比上限多吃 100 MB 記憶體才被擋下。匯入前另有一道「先看檔案大小」的檢查(import_drive_file_handler.py:173,U6 範圍),一般檔案會在那一步就擋掉。留給 U6 看 Google 文件匯出的路徑是否有大小可看。
§7

這份結果可信到什麼程度

第一層:工具正式報告(經三人面板驗證)

  • 驗證章:verified(stamp CLAUDE-SECURITY-REVISION-fe7c471f3d7c-dirty.json)
  • 研究員 2 派 2 回(全範圍 1+密碼金鑰專掃 1),failed 0、續跑 0;候選 6、去重後 6,18 票全投完、零漏投。
  • 通過 6 條:F1/F2/F3/F4 3:0 一致,F5/F6 2:1。面板主動調降 2 條(F2 高→中、F6 中→低)。
  • 工具的每一條都是讀碼推論,工具本身沒有執行任何程式。

第二層:runner 自行開檔核對(未經三人面板投票)

  • 範圍 9 支逐支通讀;卡片「重點看什麼」三條、「有人守了一半」八種樣式逐條答完。
  • 跨讀了範圍外的相關程式(只讀不報):所有呼叫同步總指揮的網址入口(7 處)、core/scheduler.py 的背景工人與續約排程、U6 範圍的建資料夾與匯入證據兩支處理器、infra/flow_control/repository/flow_control_project_repo_impl.py 的專案查詢、jedi-common 的 system_context/tenant_context 與資料庫隔離開關、common/middleware/app_mw.py 的標頭遮罩、FR-016 設計文件。
  • 實測:回呼驗證邏輯本機直接呼叫(假授權紀錄+假工作表,六種情境,見疑點②③);U5-1 在 DEV 以唯讀交易模擬兩種身分查同一個專案;U5-5 本機組查詢字串看結構。
  • DEV 資料庫唯讀查證(2026-09-25 13:09 +08,全程 BEGIN READ ONLY … ROLLBACK):drive_sync_jobs 隔離規則(四條,開啟中)、app_tenant_allowed_for_session 函式內容、專案分布在 102/131 兩家公司、drive_folder_mappings 的唯一約束。
  • 打折處:
    1. 本機 BE 當時沒在跑,所有實測都是直接呼叫函式或在資料庫層模擬,沒有對運行中的服務打真的請求。
    2. U5-1「讀」這一半:原想直接跑專案樹載入器,但本機缺雲端權杖加密金鑰、另有一個不相干的容器零件組不起來(FlowControlJobRepoImpl 缺方法),DI 啟動失敗,改成照同一段查詢條件在資料庫層模擬兩種身分。結論(系統身分查得到、公司身分查不到)不受影響,但不是端到端實跑。
    3. U5-1「寫入別家任務」與 U5-2、U5-5 都要接真的 Google 雲端硬碟才驗得出來,本棒沒有實測,是讀碼推論。
    4. 只查了 DEV,沒看 STG/POC。
§8

執行概況

項目 值
掃描目標 BE repo compliance-manager-be,branch feature/review
版本 fe7c471f3(stamp 帶 -dirty:工作區有平行線未 commit 改動;範圍 9 支檔案本身無改動,git status 確認)
工具 Claude Code 官方 claude-security plugin 0.11.0
參數 mode scan/effort low/focus attack-surface/scope 9 檔
run ID wf_32b5fcb9-978
報告目錄 CLAUDE-SECURITY-20260925-042306/(不入版控)
耗時 約 43 分鐘(2,569 秒)
agent 20 派 20 回,錯誤 0
研究員 2 派出/2 交回(全範圍 1+密碼金鑰專掃 1),failed 0
候選/面板票 6/18(零漏投)
驗證章 verified
工具正式發現 6 條(中 5、低 1);其中 2 條淨新增(U5-1、U5-2),4 條為既有工單重現
runner 自行發現 3 條(皆低),皆未經面板