Guidant AI 資安檢視總報告 · 連線清單

C16 api → Discord/Telegram(即時通知推播,依設定)

系統把流程通知(任務指派、批次完成、專案啟動)推到客戶的 Discord 頻道或 Telegram 群組走的那條線。方向是單向往外送,鑰匙是一條 webhook 網址或一個 bot token——拿到就能以系統的名義往那個頻道發話。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。

依設定 網址即憑證 可能手法 3 種 掃描命中 0 條

這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這條線在六頁 STRIDE 裡零命中——不是沒問題,是通知模組的 M16-4(「測試 Discord」填什麼網址就打什麼)在 STRIDE 歸類時標的是 C02(入口在 API),沒標這條。本頁第四段改寫「依這條線的特性可能的攻擊手法」,M16-4 以引用方式列出。個別問題修了沒不在這頁講。

§1

一、連線圖

%%{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
    ADM(["👤 租戶管理員<br/>(notify_config.update)"])
    USR(["👤 任一登入使用者"])
    subgraph MAIN["🏠 主系統 api"]
        direction TB
        CFG[("NOTIFY_CONFIG<br/>每租戶各一組<br/>DISCORD: webhook 網址<br/>TELEGRAM: bot token・chat_id")]
        EVT["流程事件<br/>指派・批次完成・專案啟動"]
        TH["背景執行緒<br/>(主執行緒先讀好設定再丟)"]
        CFG --> TH
        EVT --> TH
    end
    DC(["💬 Discord<br/>discord.com/api/webhooks/…"])
    TG(["✈️ Telegram<br/>api.telegram.org/bot…"])
    ADM -- "C02 · 填網址/token、測試" --> CFG
    USR -- "C02 · 暱稱、專案名進訊息" --> EVT
    TH == "C16 · HTTPS POST 純文字" ==> DC
    TH == "C16 · HTTPS POST 純文字" ==> TG
    classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
    classDef open fill:#FDECEC,stroke:#C0392B
    class ADM,USR,DC,TG ext
    class CFG,EVT open
C16 — api 到 Discord/Telegram。每個租戶各自一組設定(webhook 網址、bot token、chat_id)。流程事件觸發時,api 在背景執行緒把一段純文字 POST 到 Discord 官方 webhook 或 Telegram Bot API。紅色是這條線的兩個入口:網址與 token 由客戶管理員填(網址本身就是發話憑證),訊息內容夾著使用者自填的暱稱與專案名。
§2

二、這條線怎麼連

項目 現況
誰 → 誰 guidant-api → Discord 官方 webhook 端點,或 Telegram Bot API。目的地:Discord 由設定頁填的 webhook 網址決定(只放行 discord.com 系六個官方網域、https、路徑 /api/webhooks/);Telegram 固定 https://api.telegram.org/bot{token}/sendMessage
協定/埠 HTTPS,requests.post();憑證驗證是 requests 預設開。兩支都沒帶 timeout
用誰的身分 Discord:webhook 網址本身就是憑證(誰拿到誰能發);Telegram:bot token(放在網址路徑裡)+ chat_id。兩者都是客戶在自己的 Discord/Telegram 上建的
設定存哪 NOTIFY_CONFIG 群組、每租戶各一列(DISCORD/TELEGRAM 兩個 key),值含 enabled、secret(網址或 token)、chat_id。讀取 API 會刪掉 secret 再回(套件 mask_secret_value(),NOTIFY_CONFIG 在遮罩群組);落庫明文
誰能改 能力點 notify_config.update(租戶管理員層級,不是總部);「測試」同一顆能力點,密碼欄沒改時從既存列補回 secret 再試
傳什麼 一段純文字(Discord content、Telegram text 帶 parse_mode: Markdown):事件描述+使用者暱稱+專案名+任務數。不跳脫(M06-27 修時刻意只跳脫信件版,聊天頻道吃純文字維持原樣)
怎麼送 業務流程在主執行緒先讀好設定(get_channel_config 要求有 user_context,背景執行緒沒有 RLS 身分會直接拒讀),再開 threading.Thread 丟出去,不等結果
何時存在 依設定(enabled 且 secret 齊全;Telegram 另要 chat_id)。沒設時記一行 warning 略過

依據:套件 jedi_notification/infra/discord/discord_adapter.py(is_allowed_webhook_url()、_ALLOWED_HOSTS)、jedi_notification/infra/telegram/telegram_adapter.py;主專案 app/notification/service/notification_service.py(get_channel_config、兩支 send)、app/notify_config/service/notify_config_test_service.py、api/notify_config/routes/notify_config_route.py(@require_capability("notify_config.update"))、呼叫點 app/flow_control/service/project_service.py:776-782、job_batch_complete_service.py:194-200、app/flow_engine/service/workflow_execution_service.py:1225-1297;套件 jedi_system_core/plugin/contract.py(DEFAULT_SECRET_MASKED_GROUPS 含 NOTIFY_CONFIG)。

DFD 對照:DFD 寫「webhook URL/bot token(NOTIFY_CONFIG)」與實況相符。補充 DFD 沒寫的:Discord 網址有官方網域白名單(CM-2371 後)、Telegram 固定官方、守門是租戶層級能力點(每家各自設,不是全系統一份)。

§3

三、哪些功能會走這條線

群 功能 對威脅的意義
① 流程通知 批次指派任務(notify_users_batch_assigned)、批次完成彙總(job_batch_complete_service)、專案啟動(_notify_project_started)——與 C10 信件版同三個事件 訊息裡有使用者自填的暱稱與專案名,原樣送出不跳脫;Telegram 端開了 Markdown 解析
② 測試 通知設定頁「測試」按鈕(POST /notify-config/{channel}/test)→ 用畫面上的設定(密碼欄沒改就補回存著的)送一則測試訊息 目的地由操作者當場指定——Discord 有白名單擋、Telegram 固定官方

沒有第三群:這條線不收回應、不讀頻道內容、不做指令互動。

§4

四、依這條線的特性可能的攻擊手法(無掃描實例)

這條線在 STRIDE 六頁零命中。依它的特性——「網址即憑證、目的地由人填、訊息夾著別人的字、不等結果」——列出三種可能的手法。前兩種是從同模組的 M16-4(STRIDE 標 C02)與 C10 T4 延伸來的,第三種是純粹依特性推。

# 風險 威脅 駭客怎麼打 得手什麼 STRIDE 實例
T1 —(無掃描實例) 拿「測試」按鈕當跳板打內網 ① 有通知設定權限的租戶管理員打開設定頁;② 把 Discord webhook 網址填成內網位址(例如 http://10.0.0.5:8080/admin);③ 按「測試」;④ 系統替他對那個位址送一個 POST;⑤ 回應內容不會回給他,但「連不連得上」會——可以逐台探測客戶內網哪台活著 拿系統當跳板探測內網拓撲(偷不到資料,只能探) E 權限提升、I 資料外洩 (STRIDE 歸 C02)M16-4「測試 Discord 群組」填什麼網址就打什麼,見 M16 模組頁問題一覽第 4 條 ⚪
T2 —(無掃描實例) 用系統的名義在客戶頻道發釣魚訊息 ① 任一登入帳號把暱稱改成一段含連結的文字,Telegram 端可用 Markdown 寫成 [請重新登入](https://假網址);② 他去批次完成任務或啟動專案;③ 系統以 bot/webhook 身分把訊息推到客戶的頻道,顯示為系統發的;④ 頻道裡的同事點了就上當 用系統的頻道身分發釣魚訊息;Telegram Markdown 可偽裝連結文字 S 冒充身分 同型:M06-27 通知信把暱稱原樣塞進信件 ⚪(修時刻意只跳脫信件版、聊天頻道維持原樣)
T3 —(無掃描實例) webhook 網址外流後冒充系統發話 ① 攻擊者拿到 webhook 網址或 bot token(DB 備份、診斷包、設定頁截圖、Discord 端的分享);② 直接對 Discord 官方 webhook POST,不經過我們系統;③ 在客戶頻道以「稽核系統」的名義發任何訊息——假的任務指派、假的「請到此連結完成稽核」 冒充系統身分對客戶發話;客戶無從分辨 S 冒充身分 —

三種手法的共同點:這條線的憑證是一條網址——拿到就能用、不需要我們系統的任何身分(T3);而我們拿它發出去的內容又夾著別人的字(T2)。T1 是「目的地由人填」的老病,Discord 那半 CM-2371 已用白名單擋住,但同一個「測試」端點的 Telegram 半邊目的地固定官方、天然不受影響。

§5

五、我們要怎麼防、目前做到哪

對應三種手法與這條線的本質,防線分五條。沒有掃描實例,所以全部標 ◇——這些是依連線特性應有的標準防線。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。

# 防線 擋哪種威脅 目前做到哪
D1 ◇ 目的地白名單:只准官方網域、https、固定路徑前綴;host 精確比對不用 endswith;帶帳密或非預設埠一律拒 T1 ✅ Discord:is_allowed_webhook_url() 六個官方 host 精確比對、https、埠 443、路徑 /api/webhooks/,在套件層擋(測試與正式發送都過這支);不合格記 warning 只帶 host 不帶完整網址(網址即憑證)。✅ Telegram:網址寫死官方,token 只進路徑。驗證:CM-2371
D2 ◇ 使用者的字進訊息前做對應格式的跳脫。Telegram 開了 Markdown 就要跳脫 Markdown 特殊字元;Discord 純文字至少去掉 @everyone/@here 與連結語法 T2 ⚠️ 沒有。M06-27 修時決定「只跳脫信件版、聊天頻道維持原樣」;Telegram parse_mode: Markdown 仍開,暱稱裡的 [文字](網址) 會被渲染成可點連結。這是當時的取捨,記在這裡是因為它直接決定 T2 成不成立
D3 ◇ 憑證加密落庫、讀取遮罩、日誌不記網址 T3 的外流面 ✅ 讀取遮罩(mask_secret_value() 刪 secret);adapter 的 log 只記 host 不記完整網址。⚠️ 落庫明文(與 SMTP、LDAP、issue token 同一組問題,見 C10 D5)
D4 ◇ 外部呼叫有逾時、失敗有紀錄。背景執行緒丟出去不等結果是對的,但沒逾時的 requests.post 會讓執行緒永遠掛著 外部慢時執行緒累積 ⚠️ 兩支 adapter 的 requests.post() 都沒帶 timeout(requests 預設無限等);每次事件開一條 thread,Discord 掛了執行緒會堆積。失敗有記 error
D5 ◇ 訊息內容最小化:頻道是第三方 SaaS,推出去的只該有「有事發生、去系統看」,不帶客戶內部的專案名與人名 客戶資料經這條線流到 Discord/Telegram 伺服器 ⚠️ 目前訊息帶專案名、暱稱、任務數。對合規客戶(不准資料出境)這是一條沒標示的出境路徑;設定頁沒有說明

最便宜的一步:D4——兩支 requests.post() 各加 timeout=10,一行一個。D2 的 Telegram Markdown 關掉(或改 MarkdownV2 並跳脫)是第二便宜的。

§6

六、依這些防線,掃描還沒看過的地方

  • 整條線沒進掃描:零命中不是零問題——通知模組掃描時看的是信件(SMTP)那半,聊天頻道只在 M16-4 的「測試」按鈕被點到。D2、D4、D5 都是本頁依特性列的,沒有人實際打過。
  • Telegram Markdown 注入(D2):該用一個暱稱含 [連結](url) 的帳號實測一次批次完成通知,看 Telegram 端渲染出什麼。
  • @everyone 濫用(D2):Discord webhook 預設允許 @everyone——暱稱含它會讓整個伺服器被 ping。
  • 執行緒堆積(D4):Discord 或 Telegram 慢時,每個流程事件開一條永不逾時的執行緒;該在壓測時看執行緒數。
  • 設定頁的出境說明(D5):與 C13 D7 同一件事——哪些內容會離開客戶機房,文件層沒寫。

依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本,本條零命中)、docs/security-report/M16-notification.md 第 4 條(M16-4,STRIDE 標 C02)、套件 jedi_notification/infra/{discord,telegram}/*_adapter.py、主專案 app/notification/service/notification_service.py、app/notify_config/service/notify_config_test_service.py、api/notify_config/routes/notify_config_route.py、三個流程事件呼叫點、套件 jedi_system_core/plugin/contract.py、DFD Level 0。