U4 檢查結果:雲端硬碟整合——設定、授權、金鑰

U4 檢查結果:雲端硬碟整合——設定、授權、金鑰

檢查日期 2026-09-25|對應卡片 CM-2159(母卡 CM-2152)|範圍 13 個檔案、1,786 行|工具 run ID wf_a828ae6e-597|驗證章 verified

§1

🔴 一句話結論

這一棒守門檢查很多,大多也問對了問題;真正的新洞在一個沒人想到要守的地方:Google 授權完成後跳回來的那一頁,可以被塞進一段程式,點一下連結就可能被盜帳號。

  • 🔴 新發現(高風險,三人面板 3:0):Google 授權完成後會跳回一個小頁面(「回呼頁」)。這個頁面不用登入,會把網址上一個叫 error 的欄位原樣寫進頁面裡的程式碼。今年 6 月曾修過一次(改用 json.dumps 包起來),但那次修法擋不住 </script> 這種寫法。我親手送了一段惡意網址,回應原封不動帶回攻擊程式碼。落地版的前端和 API 放在同一個網址底下,登入憑證又存在瀏覽器裡,所以受害者只要在登入狀態下點一條連結,登入憑證就會被偷走;受害者如果是管理員,攻擊者就拿到管理員權限。
  • 工具另外報的 3 條都是已知問題:跨公司「重建專案資料夾」=U5-1/U6-1;根資料夾編號發給任何登入者=U5-2/U6-2 的後半段;Google 應用程式密鑰寫在對話紀錄裡=CM-1631。另有 1 條「權杖加密金鑰也寫在對話紀錄裡」被面板以 0:3 否決,否決理由是光有金鑰、拿不到資料庫裡的密文就沒用,這條也在 CM-1631 裡。
  • 我人工另補 3 條低度問題(沒經過三人面板):加密金鑰換掉後系統不會報錯,只會一直顯示「已連線」(非資安,屬於出事查不出來);授權連結可以轉給別人代按;「驗證憑證」按鈕的守門比它的註解寫的少一道。
  • 卡片點名的疑點大多不成立:權杖更新不會寫錯公司;Google 應用程式設定只有總部(平台)管理員改得動,一般使用者寫不進去;手動重試同步工作跨不了公司,資料庫層會擋;新方法 get_project_folders 沒有問題。

帶著前棒的形狀來看:U5/U6 的洞在「背景工作用可看全部公司的身分跑」。這一棒的 13 支是那條線的入口,問對問題的比例很高:寫入類都問「你是不是這家公司的管理員」,平台設定問「你是不是總部管理員」。出事的是兩個「刻意不掛權限」的入口:一是回呼頁(本來就不能掛,但漏了跳脫);二是狀態查詢(為了讓一般成員看得到「有沒有連線」,卻連根資料夾編號一起回傳)。第三個是 U5 已報過的「重建資料夾」。

§2

這一棒在檢查什麼

客戶可以把系統接上自己公司的 Google 雲端硬碟。接的時候,管理員按「連接」,系統把人帶到 Google 的同意畫面;同意後 Google 把人帶回我們的「回呼頁」,系統拿到一組授權權杖(之後代表這家公司存取雲端硬碟的憑證),加密後存進資料庫。

這一棒的 13 支程式負責這條線的「設定、授權、金鑰」:

  • 網址入口(4 支):連線狀態、產生授權網址、回呼頁、驗證憑證、同步工作管理、重建資料夾等端點。
  • 授權流程(3 支):產生授權網址、處理回呼、斷開連線、手動維運操作、驗證憑證。
  • Google 應用程式設定(1 支):我們在 Google 登記的應用程式帳號(client_id+密鑰)從哪讀。
  • 權杖與金鑰(5 支):權杖過期時換新、用哪把金鑰加解密、存取「哪家公司接了哪個 Google 帳號」這張表。

核心問題有兩個:每道守門問的問題對不對?加密金鑰從哪來、會不會出錯?

檔案 行數 角色
app/cloud_integration/service/google_drive_integration_service.py 288 授權流程主體:產生授權網址、處理回呼、斷開
api/cloud_integration/routes/google_drive_sync_route.py 260 同步維運入口:工作清單、重試、重建資料夾
app/system_config/service/google_drive_app_config_resolver.py 224 Google 應用程式帳號從哪讀、密鑰解密
infra/cloud_integration/google_drive/google_oauth_client.py 180 打 Google 的授權端點(換票、更新、撤銷)
app/cloud_integration/service/drive_sync_admin_service.py 164 同步維運操作本體
api/cloud_integration/routes/google_drive_integration_route.py 140 連線狀態、授權網址、回呼頁
domain/cloud_integration/service/google_drive_token_manager.py 131 權杖過期時換新、被撤銷時標記
app/cloud_integration/service/drive_app_credential_verify_service.py 94 「驗證憑證」按鈕的後端
infra/cloud_integration/repository/tenant_drive_integration_repo_impl.py 94 「哪家公司接了哪個 Google 帳號」這張表的讀寫
api/cloud_integration/__init__.py 84 網址註冊表
domain/cloud_integration/service/tenant_drive_integration_domain_service.py 65 連線紀錄的建立、覆寫、斷開
api/cloud_integration/routes/drive_app_credential_verify_route.py 44 「驗證憑證」網址入口
infra/cloud_integration/crypto/fernet_crypto.py 18 加解密(Fernet,一種對稱加密法)
§3

掃到什麼(總覽)

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 該補檢查的位置 嚴重度+為什麼 來源
U4-1 回呼頁把網址上的 error 原樣寫進頁面程式碼,6 月那次修法擋不住 </script> 受害者點一條連結,攻擊程式碼就在我們的網站上執行,偷走登入憑證、冒用身分;管理員中招=管理員權限外流 受害者在同一個瀏覽器裡已經登入,並且點了攻擊者給的連結。攻擊者不需要任何帳號 api/cloud_integration/routes/google_drive_integration_route.py:120(error 改成只收固定幾種錯誤代碼,不原樣寫回;至少把 < > & 轉義);:110(例外訊息 str(e) 別寫回頁面);回應加安全標頭(CSP) 🔴 高:外人不用帳號就能發動,只需要受害者點一下 工具 F1,面板 3:0;runner 實測重現
U4-2 「重建專案資料夾」收任何專案編號,不查是誰家的 見 U5-1/U6-1 見 U5-1 app/cloud_integration/service/drive_sync_admin_service.py:125 🟠 同 U6-1(U6 評高) 工具 F3,面板 3:0;U5-1/U6-1 重現,不另計
U4-3 不掛權限的「連線狀態」查詢,回傳內容含根資料夾編號 根資料夾設成「知道連結就能編輯」,公司內任何登入者都能看、改、刪全公司的證據 同公司的任一登入帳號 api/cloud_integration/routes/google_drive_integration_route.py:49(狀態回應拿掉 root_folder_id 和 google_account_email) 🟠 中 工具 F4,面板 3:0;U5-2/U6-2 重現,不另計
U4-4 Google 應用程式密鑰明文寫在對話紀錄檔 能讀程式庫的人可以冒用本產品的 Google 應用程式身分 能讀程式庫 — 🟠 中(面板從高降為中) 工具 F2,面板 3:0;CM-1631 重現,不另計
U4-5 加密金鑰換掉或弄丟後,權杖解不開,系統不報錯、仍顯示「已連線」 該公司所有同步工作重試 5 次後失敗,錯誤訊息是空白;畫面照樣顯示「已連線」,一線人員無從判斷原因 主機的 DRIVE_TOKEN_ENCRYPTION_KEY 被改、遺失,或還原了別台的資料庫 domain/cloud_integration/service/google_drive_token_manager.py:60、:66(接住解密失敗,標記成「需重新授權」並寫一句白話原因) 🟢 低(非資安,屬於出事查不出來) runner 開檔+實測(未經三人面板)
U4-6 授權網址沒有綁定「按下連接的那個瀏覽器」 攻擊者把自己公司的授權網址轉給別人,對方在 Google 同意畫面按下同意,對方的雲端硬碟就接進攻擊者的公司 攻擊者是某家公司有「建立連線」權限的管理員;受害者願意在 Google 同意畫面上按同意 app/cloud_integration/service/google_drive_integration_service.py:90-92(產生時把 state 也寫進發起者的瀏覽器);:107-110(回呼時核對,並改成「讀取與刪除一次完成」) 🟢 低:受害者要自己在 Google 畫面上點同意;未實測(要真的 Google 帳號) runner 開檔(未經三人面板)
U4-7 「驗證憑證」只檢查是不是總部的人,比同一組設定的讀寫端點少一道能力點檢查,跟它自己的註解不符 總部任何一個登入帳號(不必是管理員)都能按:看得到目前設定能不能用、回呼網址是什麼,也能拿伺服器去試任意一組 Google 憑證 總部公司的任一登入帳號 app/cloud_integration/service/drive_app_credential_verify_service.py:71(比照讀寫端點先檢查 storage-config 能力點) 🟢 低:拿不到密鑰本身,只拿得到「對或錯」 runner 開檔(未經三人面板)

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

  • U4-1(回呼頁注入)=總表第 188 項,✅ 已修(CM-2200,commit f0a740bd7,1.21.0 出貨)。
  • U4-2(重建專案資料夾)=U5-1/U6-1=總表第 192 項,✅ 已修(CM-2201,commit 73f5f90af,1.21.0 出貨)。
  • U4-3(狀態查詢回根資料夾編號):併 M24 第 8 條,🚫 裁定不修(決策者 10-01,雲端硬碟分享權限不由產品管)。
  • U4-4(Google 密鑰寫在對話紀錄):原卡 CM-1631 作廢,✅ 已修(CM-2051,commit ccbb27148,總表第 74 項)。
  • U4-6(授權連結綁瀏覽器)=總表第 190 項,✅ 已修(CM-2218,commit a671d0b64,1.21.0 出貨)。
  • U4-7(驗證憑證少一道能力點)=總表第 191 項,✅ 已修(CM-2211,commit fe928859f,1.21.0 出貨)。
  • U4-5(金鑰換掉仍顯示已連線,非資安):SUMMARY 無對應條目,查不到現況。

淨新增:高 1(U4-1)+低 3(U4-5/U4-6/U4-7,其中 U4-5 為非資安)。

§4

工具報的逐條

工具交回 5 條候選,三人面板 15 票全投完、零漏投。通過 4 條、否決 1 條,研究員 2 派 2 回、零失敗。

F1 → U4-1 🔴 回呼頁可以被塞進程式碼(高,3:0)

在哪:api/cloud_integration/routes/google_drive_integration_route.py:115-140(_callback_html),入口 :86-112。

白話說明:使用者在 Google 同意授權後,Google 會把人帶回我們的 /api/1.0/integrations/google-drive/callback。這個頁面本來就不能要求登入(進來的是 Google 轉址,不是我們的前端),網址上的 error 欄位誰都能填。程式把它丟進 json.dumps 處理後,直接插進頁面裡一段 <script> 程式碼。

json.dumps 會把雙引號、反斜線處理好,但不處理 <、>、/。瀏覽器讀網頁時,只要看到 </script> 就會把那段程式碼結束掉,不管它在不在引號裡。所以攻擊者在 error 塞進 </script><script>任意程式</script>,後半段就會變成一段新的、會被執行的程式碼。

runner 實測(不打真伺服器,用 Flask 測試用戶端掛上真正的 route 類別):送出 error=</script><script>alert(document.domain)</script>,回應 200、text/html、沒有任何安全標頭,回應原文包含 error: "</script><script>alert(document.domain)</script>"。再用 Python 內建的 HTML 解析器解析這段回應,確認被切成兩個 script 元素,其中一個的內容正是 alert(document.domain)。

為什麼偷得到登入憑證:

  • 落地版的 nginx 把前端和 /api/1.0 放在同一個網址底下(前端 repo nginx.onprem.conf:88-91),所以這段程式在「我們的網站」上執行。
  • 前端的登入憑證存在瀏覽器的 localStorage(前端 src/utils/authPersist.js:4),同網址下的程式讀得到。

沿革:2026-06-05 commit 99055ab00 的 C12 修過這支。當時是從 f'"{error}"' 改成 json.dumps,commit 訊息也寫了「專案無 CSP 兜底」。程式碼註解 :118-119 至今仍寫著「用 json.dumps 跳脫所有注入…防 Reflected XSS」,這句話不成立。我查過跨 arc 總表,沒有這一條。

觸發前提:受害者在同一個瀏覽器裡已登入,並點了攻擊者的連結。攻擊者不需要任何帳號,不需要這家公司接了雲端硬碟,也不需要知道任何編號。開發環境前端和 API 不同埠號,偷不到憑證,但頁面一樣會執行程式。

建議修法:

  1. error 只接受固定幾種錯誤代碼(例如 access_denied、missing_params),其他一律換成通用代碼,不要把使用者給的字串寫回頁面。:110 的 str(e) 也一樣。
  2. 最少的修法:在 json.dumps 之後把 <、>、& 換成 <、>、&。
  3. 這支回應加上 Content-Security-Policy,只允許頁面內這一段程式。

嚴重度:高(面板三票都評高)。唯一的門檻是「受害者要點連結」。

F3 → U4-2(=U5-1/U6-1,不另計)

工具從 U4 這一側再撈到一次「重建專案資料夾」的跨公司問題。從入口看,病灶有兩處:

  • api/cloud_integration/routes/google_drive_sync_route.py:108-110 只掛了「登入」與「公司有沒有買雲端整合」,問的是「你有沒有買」,不是「這專案是不是你的」。
  • app/cloud_integration/service/drive_sync_admin_service.py:120-123 的註解寫「FE already gates the button to project owner / manager; no extra BE check」,也就是「前端已經把按鈕藏起來了,後端不再檢查」。這正是卡片「有人守了一半」列的那一型:註解說這裡刻意不查,但上一層根本沒人真的查(前端藏按鈕不算檢查)。

同一支 route 檔的隔壁 verify-and-repair-folders(:126-156)有在 service 層檢查專案管理者(drive_project_verify_service.py:158-)。所以修法有現成的樣板可以照抄。

F4 → U4-3(=U5-2/U6-2 後半段,不另計)

api/cloud_integration/routes/google_drive_integration_route.py:32-49 的註解說明這支 GET 刻意不掛能力點,理由是「專案頁要用它判斷要不要顯示雲端區塊」,這個理由成立。問題是回應整份 DTO(資料傳輸物件,就是回給前端的那包資料)都帶出去了,裡面有 root_folder_id 和 google_account_email(app/cloud_integration/dto/google_drive_integration_dto.py:16、:47)。前端判斷要不要顯示只需要 status。

同一個模組裡守門不一致:新方法 get_project_folders 只回某一個專案的資料夾編號,卻要求 cloud_integration.read 能力點(google_drive_sync_route.py:175)。範圍更大的根資料夾編號反而不用任何能力點。

F2 → U4-4(=CM-1631,不另計)

Google 應用程式密鑰(GOCSPX-…)明文出現在 docs/conversation-history/2026-04-21-google-drive-integration/ 的多個對話紀錄檔裡。面板把嚴重度從高降為中(三張確認票都評中)。U5 已記過,屬於 CM-1631,不另計。

被否決的 1 條:權杖加密金鑰也寫在對話紀錄裡(0:3)

三位檢查員都確認 DRIVE_TOKEN_ENCRYPTION_KEY 確實出現在同一批對話紀錄裡,而且和本機開發環境的 .env 是同一把。否決理由是只有金鑰沒用,還要另外拿到資料庫裡的密文,而程式庫裡找不到任何一段真的密文。這一條在 CM-1631 的範圍內,否決不代表可以不處理。

§5

卡片點名的疑點逐條回答

以下都是 runner 自行開檔核對,沒有經過三人面板投票。DEV 查詢時間 2026-09-25 13:49(隔離規則)與 14:17(筆數)。

① 權杖加密金鑰從哪來?解密失敗怎麼辦?換新時會不會寫錯公司?

  • 金鑰從哪來:主機環境變數 DRIVE_TOKEN_ENCRYPTION_KEY(config/config.py:165),經 DI 容器(di_containers/cloud_integration/cloud_integration_containers.py:109)交給 FernetCrypto。每套安裝各自隨機產生一把(scripts/installer/install.sh:1260),不是全產品共用。

  • 換公司會不會共用一把:同一套安裝裡的所有公司共用同一把,平台的 Google 應用程式密鑰也用這把(google_drive_app_config_resolver.py:26)。這是設計選擇:公司之間的權杖靠資料庫的隔離規則分開,不是靠金鑰分開。能讀到資料庫全部資料又拿到這把金鑰的人,可以解開所有公司的權杖。不列為發現,但要記住一件事:外洩一把金鑰,受影響的是全部公司,不是單一公司。

  • 沒有換金鑰的機制:fernet_crypto.py 只用單一把 Fernet,沒有用可以同時認新舊金鑰的 MultiFernet。換金鑰=所有公司的授權一次失效。

  • 解密失敗 → 成立,列 U4-5(非資安)。我寫了一段小程式模擬「舊金鑰加密、新金鑰解密」,打 GoogleDriveTokenManager.get_valid_access_token,結果:

    • 拋出的是 cryptography 的 InvalidToken,錯誤訊息是空字串;
    • 權杖管理器只接「Google 說權杖失效」(InvalidGrantError,:69),不接解密失敗,所以連線狀態維持 CONNECTED,也不會被標成需要重新授權;
    • 背景工人把它當成暫時性錯誤(drive_sync_worker.py:132-137),重試 5 次後標失敗,存進去的錯誤訊息是 str(e),也就是空白。

    結果:畫面顯示「已連線」、同步全部靜默失敗、失敗原因欄是空的。

  • 換新權杖時會不會寫錯公司 → 不成立。get_valid_access_token(tenant_id) 用公司編號查出那一列,就地更新同一列(:49、:89-93);被撤銷時另開交易,也是用同一個公司編號重查(:115)。資料庫對 tenant_id 有唯一索引(DEV tenant_drive_integrations_tenant_id_key),一家公司只會有一列,不存在挑錯列的可能。

② 兩支 route 檔的守門,問的問題對不對?

逐個端點看「掛了什麼、問的是什麼、該問什麼」:

端點 掛了什麼 問的問題 對不對
GET /integrations/google-drive 連線狀態 登入+有買 你是這家公司的人嗎 ⚠️ 問題對,但回太多(U4-3)
DELETE 斷開連線 登入+有買+cloud_integration.delete 你是這家公司有權限的管理員嗎 ✅
POST /auth-url 產生授權網址 登入+有買+cloud_integration.create 同上 ✅(但見 U4-6)
GET /callback 回呼頁 無(刻意,Google 轉址進來) 靠一次性 state 認身分 ✅ 身分部分對;❌ 頁面輸出沒跳脫(U4-1)
POST /verify-credentials 登入+service 層總部檢查 你是總部的人嗎 ⚠️ 少問「你有沒有權限」(U4-7)
GET /sync-jobs 工作清單 登入+有買+.read 你是這家公司有權限的管理員嗎;只查自家(:55) ✅
POST /sync-jobs/<uid>/retry 登入+有買+.update 同上 ✅ 程式沒限公司,但資料庫隔離規則擋得住(見 ④)
POST /projects/<uid>/init-folders 登入+有買 只問「你有沒有買」 ❌ 該問「這專案是不是你的、你是不是它的管理者」(U4-2=U5-1)
POST /projects/<uid>/verify-and-repair-folders 登入+有買+service 層專案管理者 你是這個專案的管理者嗎 ✅
GET /projects/<uid>/folders 登入+有買+.read 你是這家公司有權限的管理員嗎;只查自家 ✅(見 ⑤)
POST /tenant/rebuild-all 登入+有買+.update 同上;只動自家 ✅
POST /webhook/register 登入+有買+.update 同上;只動自家 ✅

cloud_integration.* 四顆能力點都不是平台級(is_platform = FALSE,scripts/init/04-seed-core.sql:184-187),只配給各家公司的管理員角色。DEV 實查 2026-09-25 13:49 看到的是各公司的「系統管理員」「Admin」類角色。對「只動自家公司資料」的操作來說,這個問題問得對。

③ Google 應用程式設定從哪讀?一般使用者能不能寫進去、把整家公司導到攻擊者的 Google 應用程式?

不成立。

  • 沒有「公司層」這一層。卡片推測的「公司→原廠→環境變數」三層並不存在,實際只有兩層:總部(ROOT)那一列(google_drive_app_config_resolver.py:103-108,固定讀 SYSTEM_ROOT_TENANT_ID)和主機環境變數(:111-125)。各家公司自己存的同名設定不會被讀到。
  • 寫入走泛用系統設定端點,由 guarded_system_config_service.py:259-275 在能力點之後再加一道總部管理員檢查(旗標 DRIVE_APP_CONFIG_PLATFORM_ADMIN_ONLY = True)。一般使用者和各公司管理員都寫不進去。
  • 附帶一提(不列為發現):旗標註解說「未來改 False 就開放客戶自填」,可是解析器只讀總部那一列,就算把旗標改成 False,客戶自己填的也不會生效。這是功能上的落差,不是資安問題,以後要開放時要一起改。

④ 手動觸發同步:能不能觸發別家公司的?

  • 清單、全部重建、補登通知:一律用登入者自己的公司編號(google_drive_sync_route.py:55、:216、:252-257)。不成立。
  • 重試單一工作(:97 → drive_sync_job_repo_impl.py:175-191):程式只用工作編號查,沒帶公司條件。但 drive_sync_jobs 開了資料庫隔離,DEV 實查 2026-09-25 13:49:UPDATE 規則是「總部身分,或這筆資料的公司在你的可見範圍內」;API 連線用的 cm_app 帳號不能繞過隔離(rolbypassrls = f)。別家公司的工作編號更新到 0 筆,回 retried: false。不成立。唯一例外是總部的人本來就看得到全部,這是平台層設計。
  • 重建專案資料夾 → 成立,即 U5-1/U6-1(見上方 F3)。

⑤ 新方法 get_project_folders(掃描後才新增的)

不成立,沒有問題。 2026-09-18 commit a0ae4f378(CM-1940)新增的這支:

  • 有 @transaction(drive_project_verify_service.py:115),查詢在正確的交易範圍內;
  • 用登入者的公司編號加上專案編號查(drive_folder_mapping_repo_impl.py:53-65,三個條件都有),資料庫隔離又再擋一層。拿別家公司的專案編號來查,只會回兩個 null,不會回別家的資料夾編號;
  • 前面掛了 cloud_integration.read。

唯一值得一提的是 U4-3 講的「守門不一致」:這支範圍小卻要能力點,範圍大的根資料夾編號反而不用。

⑥「有人守了一半」逐型對照

常見型 這一棒有沒有
只驗「你是誰」沒驗「這是不是你的」 有:init-folders(U4-2)
列表有查、單筆沒查 重試單一工作程式沒查,但資料庫隔離擋住,實際不成立
守門寫在 route 層、第二支路由沒掛 沒有:__init__.py 註冊的端點都逐一核過
or 串起來其中一個永遠成立 沒有
「查不到」和「沒權限」混成同一個回應 回呼頁「公司已被刪」與「state 失效」回同一個錯(google_drive_integration_service.py:121-124),這是刻意的,避免外人探出內部狀態,屬於正確做法
背景/系統身分假設「呼叫者一定是自己人」 本棒範圍內只有回呼頁用機器身分,且公司編號取自自己寫下的一次性 state,不成立(背景工人那段屬 U5/U6)
防重放只在單一進程內有效 state 存在 Redis,多台機器共用,不成立;但「讀取」和「刪除」分兩步做(:107-110),同一個 state 同時送兩次理論上兩次都會通過。要先拿到 state 才打得到,併入 U4-6 一起修
註解說「刻意不檢查」但上一層沒人真的檢查 有:init-folders 的「FE 已擋」(U4-2);狀態查詢「讀有沒有連線不是敏感資訊」,但實際回的不只這些(U4-3)
§6

人工補的三條(未經三人面板)

U4-5 🟢 加密金鑰換掉後,系統不報錯,一直顯示「已連線」(低,非資安)

  • 在哪:domain/cloud_integration/service/google_drive_token_manager.py:60、:66 呼叫解密,只接 InvalidGrantError(:69)。
  • 怎麼驗的:runner 寫了一段模擬程式,用兩把不同的金鑰分別加密和解密,打權杖管理器,結果見上方 ①。
  • 影響:主機換過金鑰、弄丟金鑰、或把別台的資料庫還原過來時,這家公司的同步全部停擺,畫面卻顯示「已連線」,失敗原因欄是空的。一線人員看不出是金鑰問題。
  • 建議修法:在解密的兩處接住 InvalidToken,比照 _mark_revoked 把連線改成需要重新授權,並寫一句白話原因(例如「加密金鑰與資料不符,請重新連接」);背景工人把它當成終止錯誤,不要重試。
  • 嚴重度:低。這不是資安問題(攻擊者得不到任何東西),是「出事時查不出原因」。google_drive_app_config_resolver.py:30-34 的註解已經刻意要求「解不開就拋」,這個方向是對的,問題只在拋出來之後沒人接。

U4-6 🟢 授權網址沒有綁定按下「連接」的那個瀏覽器(低)

  • 在哪:app/cloud_integration/service/google_drive_integration_service.py:90-94 產生授權網址時,把「公司編號:使用者編號」存進 Redis,以一次性的 state 當鑰匙;回呼時(:107-116)只看 state,不核對「完成授權的人是不是當初按連接的那個瀏覽器」。
  • 怎麼被利用:某家公司有「建立連線」權限的管理員取得授權網址,轉寄給別人(例如別家公司的人),說「幫忙按一下」。對方在 Google 同意畫面用自己的 Google 帳號按下同意,對方的雲端硬碟就被接進攻擊者的公司:之後系統會在對方的雲端硬碟裡建資料夾、讀取變動。
  • 門檻:受害者要在 Google 的同意畫面上,對一個要求「完整雲端硬碟存取」的應用程式按同意。畫面上寫得很清楚,所以列為低。state 10 分鐘就失效。
  • 建議修法:產生授權網址時,把 state 也寫進發起者瀏覽器的 cookie,回呼時比對兩邊一致(OAuth 標準 RFC 6749 §10.12 的建議做法)。讀取和刪除改成一次完成(Redis GETDEL),順便把「同一個 state 同時送兩次」這個小縫補起來。
  • 未實測:要真的 Google 帳號走完同意流程,本機做不到。

U4-7 🟢「驗證憑證」的守門比註解寫的少一道(低)

  • 在哪:app/cloud_integration/service/drive_app_credential_verify_service.py:71 只呼叫 require_platform_admin();route(drive_app_credential_verify_route.py:30)只掛登入。
  • 落差:註解(:68-69)說這支「與這組設定的讀寫端點同一道門」。但讀寫端點是先檢查 storage-config 能力點,再檢查總部管理員(guarded_system_config_service.py:259-275)。這支只做了後半段。「總部管理員」的判定只看「是不是總部這家公司的成員」(jedi_iam/authz/platform.py:15-22,路徑只有一層就算),不看角色。
  • 影響:總部任何一個登入帳號(不必是管理員)都能:① 不帶參數按下去,知道目前存的憑證能不能用,並拿到回呼網址;② 拿伺服器去試任意一組 Google 憑證(目標網址寫死是 Google,不會變成可以任意打內網的跳板)。拿不到密鑰本身。
  • 建議修法:比照讀寫端點,先檢查 storage-config.update 再檢查總部。
  • 嚴重度:低。
§7

可信度分兩層

  1. 工具正式清單(經三人面板投票):4 條通過,全部 3:0;1 條以 0:3 否決。驗證章 verified,15 票零漏投,研究員 2 派 2 回。U4-1 我另外親手實測重現了。
  2. runner 自己追的(沒經過面板):U4-5 有實測;U4-6、U4-7 是開檔推論,U4-6 沒有實測。卡片點名的 6 個疑點都逐支開檔回答了,13 支範圍檔也都逐支通讀過。

打折處:

  • U4-1 的實測用的是 Flask 測試用戶端掛真正的 route 類別,不是打在跑的伺服器(本機 BE 沒開),也沒有開真的瀏覽器執行,是用 HTML 解析器確認 script 會被切開。「偷登入憑證」這一步是依前端程式碼推論的(同網址+localStorage),沒有端到端實跑。
  • 資料庫隔離的結論是在 DEV 查規則與帳號屬性得出的,沒有實際用 cm_app 身分跑一次跨公司的 UPDATE。
§8

執行概況

項目 數值
範圍 13 檔/1,786 行(開跑前 `git ls-files
工具參數 --effort low、focus attack-surface、scanRoot=BE repo
run ID wf_a828ae6e-597
報告目錄 CLAUDE-SECURITY-20260925-053527/(不進版控)
版本 159a2fff977c(工作區有平行線未 commit 改動,stamp 標 -dirty)
驗證章 verified,5 候選、15 票、零漏投
研究員/面板 研究員 2 派 2 回;agent 17 派 17 回、0 失敗
耗時 約 35 分鐘
子 agent 用量 約 118 萬 token
結果 工具通過 4 條(高 1、中 3),3 條為既有重現;runner 另補低 3 條