Guidant AI 資安檢視總報告 · 連線清單
使用者在產品裡送出「意見回饋」後,系統帶著公司的 GitHub/GitLab 通行證去開發團隊的問題單系統開一張單、上傳附件、之後同步狀態走的那條線。方向是本系統 → 外部問題單系統,流的是回饋內容、附件,與那把通行證。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。
這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這裡以連線為單位,把這條線的特性、掃描在這條線上抓到的攻擊手法、以及由此該有的防線整理成一頁。個別問題修了沒不在這頁講,請點條目連回 STRIDE 頁。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB'}}}%%
flowchart LR
USR(["👤 任一登入使用者"])
ADM(["👤 平台管理員"])
subgraph MAIN["🏠 主系統"]
direction TB
CFG[("ISSUE_INTEGRATE_CONFIG<br/>全系統一份,存 ROOT<br/>GITLAB: url・project・token<br/>GITHUB: project・token")]
FB["意見回饋服務<br/>新增・修改・刪除・附件"]
CFG --> FB
end
GL(["🦊 GitLab<br/>(網址由設定填)"])
GH(["🐙 GitHub"])
MITM(["🕵 站在路上的人"])
USR -- "C02 · 送回饋/改/刪" --> FB
ADM -- "C02 · 改設定" --> CFG
FB == "C15 · HTTPS + PAT<br/>開單・傳附件・關單" ==> GL
FB == "C15 · HTTPS + PAT" ==> GH
MITM -. "不驗憑證時可假扮" .-> GH
classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
classDef open fill:#FDECEC,stroke:#C0392B
class USR,ADM,GL,GH,MITM ext
class FB open
| 項目 | 現況 |
|---|---|
| 誰 → 誰 | guidant-api → GitLab(網址由設定填,預設 https://gitlab.com)或 GitHub(固定 api.github.com)。兩家各一個開關,可同時開 |
| 協定/埠 | HTTPS。GitLab 用 python-gitlab、GitHub 用 PyGithub(timeout=10、per_page=100)。兩個 SDK 的憑證驗證都是預設開,程式沒有關閉的參數(M23-4 修後) |
| 用誰的身分 | 一把個人存取權杖(PAT),全系統共用、存 ISSUE_INTEGRATE_CONFIG 那組設定在 ROOT 租戶。它是原廠(或客戶 IT)在問題單系統上的身分——拿到它等於能讀寫那個專案的程式庫與問題單 |
| 傳什麼 | 回饋標題、內容、附件檔;關單指令;讀回問題單狀態與附件清單同步回本地 |
| 誰能改設定 | 能力點 issue-integrate-config.{create,read,update,delete},由 core/plugins/system_core.py 的 group 對照表在通用系統設定端點分流守門;設定全系統一份 |
| 通行證存哪 | system_configs 的 private_token 欄位,明文 JSON 落庫;讀取 API 會把它刪掉再回(套件 mask_secret_value():ISSUE_INTEGRATE_CONFIG 在遮罩群組、private_token 在遮罩鍵名裡) |
| 何時存在 | 依設定(enable: true 且有 token)。出廠預設兩家都關 |
依據:套件 jedi_issue/infra/github.py、jedi_issue/infra/gitlab.py、jedi_issue/infra/issue/adapter/{github,gitlab}/*_issue_adapter.py(建構子)、jedi_issue/app/feedback/service/feedback_service.py(delete_feedback 順序);主專案 core/plugins/issue.py(get_integrate_config 讀 ROOT 繞 RLS)、core/plugins/system_core.py(group → 能力點對照)、套件 jedi_system_core/migrations/003-system-configs-seed.sql(預設值)。
DFD 對照:DFD 寫「PAT(ISSUE_INTEGRATE_CONFIG)」與實況相符。補充 DFD 沒寫的:GitLab 網址由設定填、GitHub 固定官方;守門是能力點不是總部層級(與 SMTP/LDAP 不同,因為這組是平台管理員維護的)。
| 群 | 功能 | 對威脅的意義 |
|---|---|---|
| ① 送回饋 → 開單 | 使用者按「意見回饋」送出 → 本地建紀錄 → 若開關開著,去 GitLab/GitHub 開單並上傳附件 | 觸發者是任一登入使用者;內容與附件由他決定,會原樣出現在開發團隊的問題單裡 |
| ② 改回饋 → 更新單 | 改標題內容、加附件 → 同步到外部單 | 改的是誰的回饋,決定誰的單被改 |
| ③ 刪回饋 → 關單 | 刪除本地回饋前,先去外部把對應的單關掉(失敗不擋本地刪除) | 跨出本系統、一旦送出收不回——開發團隊正在處理的單會被關掉 |
| ④ 列表 → 讀回狀態 | 開清單時一次撈 open+closed 兩批外部單,在記憶體比對狀態 | 每次開清單都打一次外部;外部慢或掛了清單跟著慢 |
掃描在這條線上抓到 2 條問題(M23-4、M23-2),歸納起來是兩種攻擊手法。
| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🟡 中 | 站在路上假扮 GitHub,收走通行證 | ① 攻擊者站在我們後端與 GitHub 之間(例如劫持網址解析);② 使用者照常送出一筆意見回饋;③ 系統去連「GitHub」,攻擊者回一張自簽憑證,系統明寫關閉驗證(修前)照連;④ 公司的 GitHub 通行證連同問題單內容一起送到攻擊者手上。這個模組的通行證先前已外流過一次 | 公司的 GitHub 通行證(可讀寫對應的程式庫與問題單) | I 資料外洩、S 冒充身分 | M23-4 連 GitHub 時把憑證驗證關掉 🟡 |
| T2 | 🟡 中 | 用別人的回饋去關開發團隊的單 | ① 一般員工登入,打開意見回饋清單看到別人送的回饋;② 對別人的回饋改內容或按刪除;③ 系統不比對歸屬就執行,刪除時還把開發團隊正在處理的那張外部單一起關掉;④ 操作日誌記下的是一個合法登入的人做了合法操作 | 竄改或刪掉任何人的回饋,並關掉外部問題系統上正在處理的單 | R 事後無法追查、T 竄改資料 | M23-2 任何員工都能改刪別人的意見回饋並關掉外部問題單 🟡 |
兩種手法的共同點:這條線帶的是原廠(或客戶 IT)在另一個系統上的身分,而觸發它的是任一登入使用者。T1 是「出門不確認對方是誰」(與 C10~C13 同病,但這條是明寫 verify=False 不是「沒主動開」);T2 是「跨出本系統的動作,沒先確認呼叫者有資格」——外部系統只認那把通行證,不認我們的使用者,所以我們這端的守門是唯一一道。
對應兩種威脅與這條線的本質,防線分五條。D1、D2 直接對應掃描抓到的手法;標 ◇ 的(D3~D5)是依這條線的特性補的標準防線,掃描範圍沒涵蓋、沒有實例。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。
| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 | 永遠不關憑證驗證。用 SDK 預設;程式裡不得出現 verify=False、ssl_verify=False |
T1 | ✅ 兩處 Github(..., verify=False) 已拿掉(CM-2066),GitLab 那半查過本來就有驗。⚠️ 靠 code review 不讓它回來;沒有 lint 規則或守衛測試掃 verify=False |
| D2 | 跨出本系統的動作之前先比對歸屬,非本人一次外部呼叫都不送。改、刪、刪附件疊 _assert_owner;六支端點補能力點;清單加「我的/全部」範圍 |
T2 | ✅ delete_feedback() 的 _assert_owner 放在關外部單之前(程式註解明寫理由);非本人 403 FEEDBACK_403002,取不到身分一律擋;能力點 feedback.{manage,view,create,update,delete}。驗證:六支端點 × 三種身分 18 組合 |
| D3 ◇ | 通行證加密落庫、讀取遮罩。比照 AI 金鑰與 Drive 密鑰的三件套(遮罩/補回/加密) | 讀得到設定表或設定 API 的人 | ✅ 讀取遮罩有:套件 mask_secret_value() 對 ISSUE_INTEGRATE_CONFIG 刪掉 private_token 再回,dict 與 DTO 兩種形狀都遮;寫入時密碼欄沒改會從既有列補回。⚠️ 落庫是明文——AI 金鑰與 Drive 密鑰有加密(AI_PROVIDER_ENCRYPTION_KEY/Fernet),這組與 SMTP、LDAP、Discord 一樣沒有;能讀 DB 或 DB 備份的人直接拿到 token |
| D4 ◇ | 外部單裡不帶客戶的敏感內容。回饋內容與附件是使用者自填,可能夾著客戶內部資料、截圖含個資;進外部系統前該提醒或過濾 | 客戶資料經這條線流到開發團隊的問題單系統(第三方 SaaS) | ⚠️ 沒有過濾或提醒;回饋表單沒有「此內容會送到外部系統」的說明。合規問卷會問 |
| D5 ◇ | 外部呼叫有逾時、失敗不拖垮本地操作。開清單一次撈兩批外部單,外部慢清單就慢 | 外部系統掛了讓本地功能跟著掛 | ✅ GitHub timeout=10;關單失敗只記 warning 不擋本地刪除。⚠️ GitLab client 建構沒帶 timeout(python-gitlab 預設不逾時);清單同步每次都打外部,沒有快取 |
最便宜的一步:D5 的 GitLab 逾時——gitlab.Gitlab(..., timeout=10) 一個參數,與 GitHub 那半對齊;外部掛了不至於把 gunicorn worker 卡滿兩分鐘。
gitlab.Gitlab(url, private_token, api_version) 三個參數,沒 timeout;GitLab 掛了開清單會卡到 gunicorn 砍(120 秒)。url 由平台管理員填、沒驗證格式——平台管理員是可信角色,但 http:// 也收,與 C13 D6、C19 的 scheme 白名單不一致。依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本)、套件 jedi-issue(infra/github.py、infra/gitlab.py、infra/issue/adapter/、app/feedback/service/feedback_service.py)、主專案 core/plugins/issue.py、core/plugins/system_core.py、套件 jedi_system_core/migrations/003-system-configs-seed.sql、DFD Level 0。