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

C12 api → 客戶資安監控系統(syslog/GELF 日誌轉送,依設定)

系統把自己的操作日誌與應用紀錄即時送到客戶自己的資安監控系統(SIEM)走的那條線。方向是單向往外送、不收東西回來;內容是誰在什麼時間做了什麼。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。

依設定 預設 UDP 明文 威脅 3 種 掃描命中 2 條

這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這裡以連線為單位,把這條線的特性、掃描在這條線上抓到的攻擊手法、以及由此該有的防線整理成一頁。個別問題修了沒不在這頁講,請點條目連回 STRIDE 頁。

§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
    ANY(["👤 任何人<br/>(登入頁不需帳號)"])
    ADM(["👤 客戶管理員<br/>(總部層級)"])
    subgraph MAIN["🏠 主系統 api"]
        direction TB
        CFG[("轉送設定<br/>全系統一份<br/>位址・埠・syslog/GELF<br/>UDP/TCP・加密")]
        LOGS["各層 logger<br/>操作日誌+應用紀錄"]
        Q["記憶體佇列<br/>(滿了丟棄,不阻塞)"]
        FMT["格式化器<br/>控制字元轉義"]
        LOGS --> Q --> FMT
        CFG --> FMT
    end
    SIEM(["📡 客戶 SIEM<br/>(位址由設定頁填)"])
    SNIFF(["🕵 同網段的人"])
    ADM -- "C02 · 改設定/測試發送" --> CFG
    ANY -- "C02 · 帳號欄填什麼就記什麼" --> LOGS
    FMT == "C12 · syslog RFC5424 或 GELF<br/>UDP/TCP/TCP+TLS" ==> SIEM
    SNIFF -. "明文時可側錄" .-> SIEM
    classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
    classDef open fill:#FDECEC,stroke:#C0392B
    class ANY,ADM,SIEM,SNIFF ext
    class CFG,LOGS open
C12 — api 到客戶監控系統。api 各層的 log 先進記憶體佇列,背景執行緒再往外送(網路慢不影響 API 回應)。紅色是這條線上的兩個入口:轉送去向由設定頁填(總部層級),而日誌內容裡夾著「任何人在任何欄位填的字」——登入頁的帳號欄不需要帳號就能填。
§2

二、這條線怎麼連

項目 現況
誰 → 誰 guidant-api(以及同一套程式的 socketio/worker 行程,各自掛一條)→ 客戶的 SIEM。位址與埠由客戶在「日誌轉送」設定頁填
協定/埠 格式二選一:syslog(RFC 5424)或 GELF;傳輸二選一:UDP 或 TCP。出廠預設 protocol=syslog, transport=udp、未啟用。埠由設定決定(STG 設 5140)
加密 獨立開關 use_tls,只有 TCP 能開(服務層與資料庫約束兩邊都擋 UDP)。開了憑證驗證恆開、沒有「不驗」選項(build_ssl_context():CERT_REQUIRED + check_hostname),自簽憑證改貼 tls_ca_cert。預設關
傳什麼 兩條流各自有開關:① 稽核事件(走 audit() 埋點、帶 event_code 的:登入、建改刪、改設定…);② 應用紀錄(其餘 log)。送出前格式化器把換行、歸位、定位、反斜線等控制字元轉成看得見的轉義字元(_escape_control_chars);GELF 走 JSON 天然轉義
怎麼驗對方身分 不加密時無(UDP 連對方收沒收到都不知道);TCP+TLS 時驗憑證
誰能改目的地 讀:能力點 log-forwarding.read;寫與「測試發送」:能力點 log-forwarding.update +總部層級(require_tenant_hq,寫在 core/plugins/api_log.py 的 _forwarding_guard(hq_only=True))。設定全系統一份(tenant_id 為 NULL 那列)
怎麼送 各層 logger 掛 QueueHandler 丟進記憶體佇列,背景 QueueListener 執行緒做 socket I/O;佇列有上限、滿了丟新紀錄不阻塞。設定改了由每個行程的 watcher 每 30 秒重讀、指紋變了才重掛
何時存在 依設定(enabled 且位址與埠齊全)

依據:套件 jedi-log/jedi_api_log/forwarding/common/forwarder.py(build_target_handler()、_escape_control_chars()、佇列與分流)、forwarding/common/settings.py(預設值、use_tls 只限 TCP)、forwarding/common/tls.py(CERT_REQUIRED 恆開)、主專案 core/plugins/api_log.py(能力點+總部層級守門)。

DFD 對照:DFD 寫「無;UDP 明文(TLS 選項 CM-2369 後有)」與實況相符;補充一點 DFD 沒寫的:寫入守門是總部層級(M09-1 修後),子公司管理員改不動轉送去向。

§3

三、哪些功能會走這條線

這條線不承載「功能」,承載的是所有功能的紀錄。對威脅分析要分的是「紀錄裡裝了什麼、誰能往裡面寫字」:

群 內容 對威脅的意義
① 稽核事件流 登入成功/失敗、帳號鎖定、建改刪資料、改系統設定、改轉送設定本身——每筆帶操作者、時間、來源 IP、事件代碼 這是客戶 SIEM 真正要的東西;被側錄等於把「誰在做什麼」交出去,被偽造等於客戶的稽核證據被污染
② 應用紀錄流 各層 logger 的一般紀錄(含錯誤堆疊、請求路徑) 量大、內容雜;錯誤訊息可能帶內部路徑與結構
③ 紀錄裡的使用者自填欄位 登入帳號欄(不需帳號就能填)、暱稱、專案名、任何被寫進 log message 的輸入 這是「任何人往客戶 SIEM 寫字」的入口;格式化器是唯一的關卡
④ 測試發送 設定頁「測試」按鈕依已存設定對外送一筆 用的是存著的設定,不是畫面上的;要先存才能測,與郵件、LDAP 不同
§4

四、這條線會帶來什麼威脅、駭客怎麼打

掃描在這條線上抓到 2 條問題(M09-3、M09-4),歸納起來是兩種攻擊手法;另加一種依這條線特性必然成立、但掃描歸在 C02 的手法(T3,實例 M09-1 標的是 C02,這裡只引用)。

# 風險 威脅 駭客怎麼打 得手什麼 STRIDE 實例
T1 🟡 中 在路上側錄全部日誌 ① 攻擊者在客戶機房同一個網段,或控制了一台中間的網路設備;② 側錄後端送往 SIEM 的封包;③ 預設 UDP 明文,每一筆都直接讀——誰在什麼時間登入、改了什麼設定、哪個專案在被稽核;④ 客戶就算 SIEM 收得了加密,設定頁上也沒有可開的選項(修前) 整份系統日誌(帳號、操作、稽核事件) I 資料外洩 M09-3 日誌轉送只有兩個選項,兩個都是明文 🟡
T2 🟡 中 往客戶的 SIEM 塞假紀錄 ① 不需要帳號,打開登入頁;② 帳號欄填「隨便一個名字+換行+一段假日誌」,例如偽造成「某某管理員授予了超級權限」;③ 登入失敗,這個帳號字串原樣寫進日誌並轉送出去;④ 客戶的 SIEM 把換行當下一筆,第二筆內容完全由攻擊者決定 在客戶的資安監控系統裡憑空捏造稽核紀錄;或反過來用大量假紀錄淹沒真的 R 事後無法追查、T 竄改資料 M09-4 在欄位裡塞換行,就能往客戶監控系統塞偽造日誌 🟡
T3 🟠 高 把轉送去向改成自己的機器 ① 拿一個能改轉送設定的帳號(修前:任何一家客戶的管理員,因為設定全系統一份、守門沒分層級);② 把位址改成自己控制的主機;③ 從此全系統所有客戶的操作日誌即時送到他那裡 全站所有客戶的稽核事件,持續不斷 I 資料外洩、E 權限提升 (掃描歸 C02)M09-1 任何一家客戶的管理員都改得動全系統的日誌轉送去向 🟠

三種手法的共同點:這條線只往外送、不收回應,所以攻擊者得手的不是「進我們系統」,而是「看到或污染客戶的紀錄」。T1、T3 都是「目的地與路徑不可信」(與 C10、C11 同病);T2 是「紀錄裡裝著任何人寫的字,而對方用換行切紀錄」——這一種只有日誌這條線才有。

§5

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

對應三種威脅與這條線的本質,防線分五條。D1~D3 直接對應三種手法;標 ◇ 的(D4、D5)是依這條線的特性補的標準防線,掃描範圍沒涵蓋、沒有實例。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。

# 防線 擋哪種威脅 目前做到哪
D1 提供加密選項,且加密時憑證驗證恆開。自簽憑證改貼信任憑證,不給「不驗」開關;加密設定變更要觸發重掛,否則畫面說加密、線上仍明文 T1 ✅ use_tls 開關(只限 TCP);build_ssl_context() 恆 CERT_REQUIRED;syslog 走 TlsSysLogHandler 覆寫建連線那步(斷線重連也加密);GELF 交握失敗往上報不退回明文;加密欄位納入設定指紋。驗證:自簽憑證起真的加密收件端實跑四種情形。⚠️ 預設關、預設 UDP——客戶不改就是明文
D2 送出前把控制字元編碼成看得見的轉義字元。換行、歸位、定位、反斜線一律轉義,內容一個位元都不少;在格式化器做(出口),不在記錄點做(進來時照實記是對的) T2 ✅ _escape_control_chars() 接在 RFC 5424 formatter;GELF 走 JSON 天然轉義。驗證:塞換行與定位字元的字串經格式化器輸出零換行、內容完整。⚠️ 只擋「切紀錄」,不擋「內容本身是假的」——攻擊者仍能在一筆紀錄裡寫「管理員授予超級權限」這種字,SIEM 的關鍵字規則可能誤判;那要靠 SIEM 端看 event_code 不看自由文字
D3 轉送去向的寫入要總部層級。設定全系統一份,子公司管理員與別家客戶的總部都改不動 T3 ✅ _forwarding_guard(hq_only=True):寫入與測試發送都疊 require_tenant_hq()(軸⑦);讀取只要能力點。「誰算總部」用 common.authz 單一判斷點(M22-4 修後擋得住別家總部)
D4 ◇ 紀錄送出前遮罩敏感內容。密碼、權杖、個資在進佇列前遮掉(本地 log 已遮的,轉送也要遮) T1 的損害上限 ⚠️ 沒做。設定表有 masking_enabled 欄位、快照會讀,但 forwarder.py 裡沒有任何地方用到;API 回 masking_available: false,前端據此不渲染這個開關(刻意沒騙人,是功能未做)。轉送的紀錄遮不遮,取決於本地 logger 進來前有沒有遮(C01 T2 那套 mark_password)
D5 ◇ 對方收到了嗎——送達確認與斷線告警。UDP 送出去就算;TCP 至少知道連線斷了 設定錯、SIEM 掛了、路徑被截,三種情況在 UDP 下全都靜默 ⚠️ UDP 無任何回饋;TCP 斷線時 handler 重連但沒有告警進操作日誌或畫面。佇列滿了丟紀錄也是靜默(設計上刻意不阻塞 API,但「丟了多少」沒計數)

最便宜的一步:D4——masking_enabled 欄位、快照讀取、前端開關(目前隱藏)三樣都已就位,只差 forwarder 真的讀它並接上既有的遮罩函式,再把 masking_available 翻成 true。

§6

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

  • 轉送遮罩尚未實作(D4):欄位與開關就位但 forwarder 沒接;目前靠本地 log 進來前已遮。該確認「本地沒遮到的」(例如 pydantic 物件格式化進 log,C10 T3 那型)轉送出去會不會帶祕密。
  • 丟棄計數與斷線告警(D5):佇列滿了丟、UDP 送丟、TCP 斷線,三種靜默失敗都沒有可見訊號。客戶 SIEM 端會以為「這段時間沒事」。
  • 應用紀錄流裡的錯誤訊息:第②群把各層 log 全送,錯誤堆疊可能帶內部路徑、SQL 片段(C02 T8 的回應面已修,log 面沒盤)。
  • 轉送設定本身的稽核:改轉送去向這件事記在 app.log_forwarding logger,宿主 forwarding_audit_logger_name 指到它,但該 logger 不在被轉送的樹裡(core/plugins/api_log.py 明寫「也不會被轉發出去」)——改了去向的那一筆,新去向看不到。

依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本)、套件 jedi-log/jedi_api_log/forwarding/common/(forwarder.py、settings.py、tls.py、settings_reader.py)、主專案 core/plugins/api_log.py、DFD Level 0。T3 的實例 M09-1 在 STRIDE 頁標的是 C02(入口在 API),本頁引用是因為它改的是這條線的去向。