Guidant AI 資安檢視總報告 · 連線清單
客戶建立「檢測基準」時不上傳壓縮檔、改填一個網址,系統去那個網址把規則包下載回來解析走的那條線。它是系統替使用者打任意網址的原語——下載回來的東西會被交給外部程式(cinc-auditor)解析,解析出的清單給人看,網址之後還會隨派工交給客戶機房的代理程式自己再下載一次(C24)。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。
這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這裡以連線為單位整理。3 條命中裡 M03-1 是「下載回來的內容交給外部程式時被當程式碼執行」——它對上傳型與網址型兩種來源都成立,STRIDE 頁同時標 C02 與 C19;M03-5 同時標 C19 與 C24(代理程式那條)。個別問題修了沒不在這頁講。
%%{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/>(建檢測基準)"])
subgraph MAIN["🏠 主系統 api"]
direction TB
REG["登記 url 型基準<br/>只收 https"]
FETCH["safe_http_fetch<br/>五道防線・60 秒・50MB"]
SHA["算 sha256<br/>隨派工下發給 agent"]
LIM["共用解析層<br/>檔數/解開總量/膨脹倍數<br/>inspec.yml 含 <% 拒收"]
CINC["cinc-auditor<br/>(外部程式,讀 inspec.yml 當 ERB 樣板)"]
REG --> FETCH --> SHA --> LIM --> CINC
end
URL(["🌐 規則包網址<br/>(客戶填的任何 https 位址)"])
INTRA(["🏢 客戶內網/雲端 metadata"])
AGENT(["🤖 客戶機房 agent<br/>C24 自己再下載一次"])
ADM -- "C02 · 填網址" --> REG
FETCH == "C19 · HTTPS GET(IP 釘住)" ==> URL
FETCH -. "被擋:私有段/http/file://" .-> INTRA
SHA -. "派工帶 url+sha256" .-> AGENT
classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
classDef open fill:#FDECEC,stroke:#C0392B
class ADM,URL,INTRA,AGENT ext
class REG,CINC open
| 項目 | 現況 |
|---|---|
| 誰 → 誰 | guidant-api(抽取在背景執行緒,不握 DB 連線)→ 客戶管理員填的網址。沒有網域白名單(決策者裁示:新增來源就要改設定,維護成本大於收益) |
| 協定/埠 | 只准 https(ALLOWED_SCHEMES = ("https",),登記與下載兩處都用同一份白名單);埠預設 443、可自填。http:// 登記時回 DETECTION_TOOLS_400013;升級前既有的 http 基準不強制失效,抽取時寫醒目警告 |
| 五道防線 | ① scheme 白名單;② 解析後的每一顆 IP 都不得落私有/保留段(ipaddress 內建旗標:RFC1918、loopback、link-local 含雲端 metadata 169.254.169.254、multicast、保留、IPv4-mapped IPv6 先攤回 v4);③ IP 釘住——用檢查過的那顆 IP 連線、Host 與 SNI 另帶原 hostname,憑證驗證對原 hostname 生效(防 DNS rebinding);④ 邊收邊算、超過 50MB 即斷(DETECTION_PROFILE_MAX_MB);⑤ 重導向 ≤3 跳、每跳重跑前四道 |
| 逾時 | 下載 60 秒;清單頁的「內容變了沒」探測(HEAD,零 body)5 秒,走同一套防線 |
| 下載後 | 算 sha256 落庫,隨派工 build_payload() 下發給代理程式對帳;交共用解析層 _read_entries()——三道防炸彈上限(檔數 10,000/解開 500MB/膨脹 200 倍,archive_limits.py);_repack_flat() 交 cinc-auditor 前讀 inspec.yml,含 <% 拒收 |
| 錯誤回顯 | 所有失敗收斂成 SafeFetchError +自己寫的中文訊息,httpx 的原始錯誤(含內部位址)只進 log——因為抽取失敗訊息會回顯到前端,是一條資訊回傳通道 |
| 誰能建 | 能力點 detection-profile.*(軸④,is_platform=false,租戶管理員能建自己的基準);公版要平台管理員(service 層 _guard_system_writable);另有軸⑥授權守門 require_license("detection-profile") |
| 何時存在 | url 型 profile 建立/改版/手動重新解析時;清單頁開啟時另發一次 HEAD |
依據:套件 jedi_detection/common/safe_http_fetch.py(模組 docstring 五道防線、_validate_and_pin()、_is_blocked_ip()、_read_with_limit()、DEFAULT_TIMEOUT_SEC/DEFAULT_MAX_REDIRECTS)、app/service/detection_profile_extraction_service.py(_download()、source_sha256、session 之外執行)、app/service/detection_profile_service.py:960(登記只收 https)、common/archive_limits.py、common/profile_extractor/inspec.py(_ERB_MARKER)、common/detection_profile_ref.py(build_payload 帶 sha256)、api/routes/detection_profile_route.py(守門三層);主專案 config/config.py:200-206(四個上限)。
DFD 對照:DFD 寫「HTTPS(只收 https,CM-2057);第一次抽取、算 sha256」與實況相符。補充 DFD 沒寫的:清單頁開啟時另有一次 HEAD 探測(也走這條線、同一套防線);私有段 IP 與 DNS rebinding 的防護在這一支。
| 群 | 功能 | 對威脅的意義 |
|---|---|---|
| ① 建立/改版 url 型基準 | POST /detection-tool-profiles(網址型走 JSON)、/{uid}/new-version、PATCH /detection-tool-profile-versions/{uid}/source → 背景下載+解析 |
觸發者是客戶管理員;填的網址決定系統去連誰。下載回來的東西會進外部解析程式 |
| ② 手動重新解析 | POST /detection-tool-profile-versions/{uid}/extraction |
同一條下載路徑再跑一次;併行上限 4(DETECTION_PROFILE_EXTRACTION_MAX_CONCURRENT,M03-10 修後) |
| ③ 清單頁探測 | list_controls() 對 url 型來源發 HEAD 比對 ETag,一樣就直接顯示現有清單 |
零 body 但同樣是「替人打網址」;SSRF 風險與 GET 相同,少一道防線就是換動詞繞過 |
| ④ 派工下發 | build_payload() 把 url+sha256 交給代理程式,代理程式走 C24 自己下載執行 |
這條線算的 sha256 是代理程式對帳的唯一依據;代理端實際比對尚未做(CM-2402) |
掃描在這條線上抓到 3 條問題(M03-3、M03-1、M03-5),歸納起來是三種攻擊手法。前提:攻擊者有一個能建檢測基準的帳號(客戶管理員);T3 的第二階段攻擊者站在客戶機房的網路路徑上。
| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🟡 中 | 網址那條路繞過上傳那條路的上限 | ① 有建立檢測基準權限的帳號;② 在自己的網站放一個 45MB、解開數十 GB 的壓縮檔;③ 新增檢測基準時選「網址」、填這個網址;④ 系統下載回來直接解開,上傳那條路的三道檢查完全沒跑(修前);⑤ 伺服器記憶體被吃光,所有客戶一起停擺 | 讓整個產品對所有客戶停擺 | D 讓服務停擺 | M03-3 網址型規則包完全繞過壓縮檔的三道上限 🟡 |
| T2 | 🟠 高 | 下載回來的設定檔被外部程式當程式碼執行 | ① 任一客戶的管理員;② 做一包檢測規則,在 inspec.yml 寫一段 Ruby 樣板(<% … %>);③ 放在網址上讓系統下載(或直接上傳),結構全部正常;④ 系統排解析,cinc-auditor 一讀設定檔就以後端程序身分把那段程式跑起來;⑤ 讀出系統密鑰、把資料庫身分提權成超級管理員,改寫全平台客戶的資料 |
整台後端主機的權限;改寫全平台客戶資料 | E 權限提升、T 竄改資料 | M03-1 客戶上傳的檢測規則包會被外部工具當成程式碼執行 🟠 |
| T3 | 🟡 中 | 網址不加密、沒指紋,代理程式拿到什麼就跑什麼 | ① 客戶管理員用一個 http 網址建了檢測基準(修前收 http);② 攻擊者站在代理程式下載的網路路徑上;③ 代理程式領到掃描工作去下載時,回一包換過的規則;④ 代理程式手上有客戶主機帳密,直接執行那包規則,掃描報告也一起造假;⑤ 因為不加密,連破解都不用 | 在客戶內網的代理程式上執行任意程式碼;偽造掃描結果 | T 竄改資料、E 權限提升 | M03-5 網址型規則包放行不加密連線、也不記指紋 🟡 |
三種手法的共同點:這條線把「外部網址的內容」變成「系統信任的規則」——T1 是內容太大、T2 是內容有程式、T3 是內容被換。三者都是「內容不可信」的不同面向,而「網址由人填」只是讓攻擊者更容易把內容送進來。有一種 SSRF 手法(填內網或雲端 metadata 位址讓系統替你打)在掃描範圍內沒有實例,因為五道防線在掃描前就已到位——列在第五段 D4。
對應三種威脅與這條線的本質,防線分六條。D1~D3 直接對應掃描抓到的手法;標 ◇ 的(D4~D6)是依這條線的特性補的標準防線,掃描範圍沒涵蓋、沒有實例。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。
| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 | 資源上限放在兩種來源唯一共用的那一層,不在各入口各補一次。檔數、解開總量、膨脹倍數三道;下載本身另有 50MB 邊收邊算 | T1 | ✅ archive_limits.py 接進 _read_entries(),上傳與網址兩路、未來第三路都天然涵蓋;刻意不在網址分支另補(補第二條路下次加來源照樣漏)。驗證:隨包規則包重打包前後一致;2MB 宣告 500MB 被擋。另:併行解析上限 4(M03-10) |
| D2 | 交給外部程式之前看內容,不只驗結構。inspec.yml 含 ERB 樣板記號就拒收;擋點在兩路共用層 |
T2 | ✅ _repack_flat() 讀 inspec.yml,含 <% 拒收並回明確訊息;開工前實查隨包 13 筆基準與 8 支內建規則包都沒用樣板。⚠️ 已知打折:只看 inspec.yml 一個檔,若 cinc-auditor 對 controls/*.rb 也做樣板渲染則擋不住;長期正解「把外部程式關進沙箱」未做——cinc-auditor 仍以後端程序身分跑 |
| D3 | 網址只收 https,第一次抽取記指紋,派工帶給代理程式對帳 | T3 | ✅ 登記白名單取 safe_http_fetch.ALLOWED_SCHEMES 不另立一份,http 回 DETECTION_TOOLS_400013;既有 http 基準不強制失效但抽取時寫警告;source_sha256 落庫、build_payload() 下發。⚠️ 代理程式端實際比對指紋尚未做(CM-2402 另開小卡)——目前指紋只是「記了」,沒有人在對 |
| D4 ◇ | SSRF 五道防線:scheme 白名單、每顆 IP 查禁區、IP 釘住、大小上限、重導向每跳重驗;HEAD 探測走同一套 | 填內網或雲端 metadata 位址讓系統替你打(掃描範圍內無實例) | ✅ safe_http_fetch.py 全部到位,模組 docstring 逐條說明「少了會怎樣」;DNS rebinding 以 IP 釘住擋、SNI 帶原 hostname 憑證驗證仍生效(已實測改 SNI 會 Hostname mismatch);多顆 A record 全部都要過;錯誤收斂成中文訊息不回顯 httpx 原文。這是全 codebase 唯一一支完整的 SSRF 防護,C13 D6、C15 的網址欄都還沒用它 |
| D5 ◇ | 外部程式關進沙箱。cinc-auditor 以獨立容器、無網路、唯讀檔案系統、最小權限跑,不以後端程序身分 | T2 的根治(D2 是治標) | ⚠️ 沒有。證據拆檔那條已有 guidant-sandbox 容器(C08),cinc-auditor 解析沒套同一個模式。M03-1 修正時明寫「長期正解未做」 |
| D6 ◇ | url 型基準的內容會漂,清單頁要說得出「這是哪一刻的快照」 | 使用者看到的清單與代理程式實際跑的不一致 | ✅ 清單頁 HEAD 探測 ETag,變了就提示重新解析;sha256 記的是「平台解析的是哪一份」。⚠️ 探測只比 ETag/Last-Modified,對方伺服器不給這兩個標頭就比不出來 |
最便宜的一步:D3 的代理端比對——sha256 已經隨派工送到代理程式手上,差的是代理程式下載後算一次、不同就拒跑;這是 CM-2402 的範圍。D5 最有價值但是一個獨立的基礎建設工作。
controls/*.rb 的樣板渲染(D2):M03-1 修正自承打折——只擋 inspec.yml。該實際做一包只有 controls/x.rb 含 <% 的規則包餵 cinc-auditor,看它跑不跑。.env。safe_http_fetch 是全 codebase 唯一完整的 SSRF 防護;AI 服務位址(C13)、GitLab 網址(C15)、Drive webhook 公開網址(GOOGLE_DRIVE_APP_CONFIG.public_base_url)都是人填的網址、都沒走它。依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本)、套件 jedi-detection(common/safe_http_fetch.py、common/archive_limits.py、common/profile_extractor/inspec.py、common/detection_profile_ref.py、app/service/detection_profile_extraction_service.py、app/service/detection_profile_service.py、api/routes/detection_profile_route.py)、主專案 config/config.py、DFD Level 0。M03-1 同時標 C02(上傳型),M03-5 同時標 C24(代理程式下載),本頁各歸一種手法。