Guidant AI 資安檢視總報告 · OWASP Top 10(2025)

A03 軟體供應鏈失效(Software Supply Chain Failures)

命中這一類的 4 件,還原成模組頁的 5 條原始條目。每一條用白話寫出:在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。

🟡 4 ⚪ 1 已修 5
§1

這一類是什麼

做軟體的過程被動了手腳——建置、發佈、更新軟體時用到的第三方程式、工具或相依套件有弱點或被惡意修改,壞東西就跟著正常出貨流程一起裝到客戶手上。OWASP Top 10:2025 新增這一類,由舊版「使用含已知漏洞的元件」擴大而來。

這次掃描的 5 條都長在同一條線上:我們的打包機 → 從套件倉庫抓元件 → 做出客戶要安裝的檔案。破口分兩種形狀:

  • 倉庫的鑰匙外流(M17-10、M15-6、M23-6):能往公司套件倉庫推東西的帳密寫進了版本控制,任何讀得到程式碼的人都能推一個帶後門的元件上去。
  • 抓元件時不驗貨(M23-5、M07-12):沒鎖版本、沒比對指紋,路上被掉包或上游被掉包都照單全收。

這一類的共同特徵是:攻擊者不必打我們上線中的系統,只要在出貨前的任何一環動手腳,產品本身就會幫他把東西送進客戶機房。

為什麼是 5 條不是 4 件:總結問題總表的 #73 是同一份交接文件、同一行帳密,被公告與 AI 儀表板兩個模組各撈到一次,總表併成一件;本頁拆回模組頁原始條目,兩條都列。標題括號內是總表編號,可回去對照。

§3

M17-10 🟡 套件倉庫與檔案儲存的帳密寫在交接文件裡

欄位 內容
嚴重度 🟡 中(總表 #73;STRIDE:竄改資料、權限提升)
在哪裡 公告模組檢視時順手做的密碼掃描撈到的(與公告功能本身無關):一份分散式檔案代理程式(FR-039)的開發交接文件,裡面明文寫著公司內部套件倉庫與檔案儲存服務的帳密
攻擊面位置 主專案的程式碼倉庫——這份文件與它產生的網頁、文件站鏡像都進了版本控制。任何讀得到程式碼的人(在職同仁、外包、離職者、拿到備份的人)都構成攻擊面,不需要系統帳號。要真的用上帳密,還需要連得到公司內網
信任邊界位置 「讀得到程式碼的人」↔︎「有權往套件倉庫發布元件的人」。發布權應該只在打包機與發版的人手上(帳密放在打包機上不進版控的 .secrets/nexus.env);當時交接文件把帳密原文抄了進去,還寫著「已加入 gitignore」——被忽略的是那個設定檔,不是這份文件,等於這條邊界被文件自己打穿
元件端點位置 外流處:docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md(+同目錄產生的 index.html、docs/features-site/ 鏡像)。被打穿的發布口:套件 monorepo 各套件 publish.sh/根目錄 publish_all.sh 用的 poetry publish --build -r nexus;吃這個倉庫的安裝端:主專案 pyproject.toml 的 [[tool.poetry.source]] nexus(…/repository/pypi-group/simple)
駭客怎麼打 ① 任何能讀主專案程式碼的人,打開這份交接文件;② 照抄套件倉庫的管理員帳密,在內網登入公司套件倉庫;③ 以管理員身分上傳一個看起來正常、但夾帶後門的內部套件新版本;④ 下一次有人升級套件、打包機重建時,這個版本被當成公司自己的元件裝進產品,跟著安裝包送到客戶機房;⑤ 同一份文件裡的檔案儲存金鑰,則可以直接讀寫存放在那裡的稽核證據
得手什麼 往客戶安裝包裡塞自己程式碼的位置,以及開發環境檔案儲存裡的稽核證據
修正的做法 ① 換發:套件倉庫與檔案儲存的帳密都已換發,外流的舊值失效(屬環境操作,不在 commit 裡);② 清出版控:CM-2048 先改來源 md——帳密改寫成「請查部署文件」,同時改掉「已加入 gitignore」這句誤導敘述,註明日後這類憑證只寫指路不寫值;③ 依序重建該文件的網頁、同步文件站鏡像,最後補清對話紀錄裡的殘留——依較寬的搜尋多清出 7 個卡片沒列到的檔(同一組檔案儲存密碼散落在幾支對話紀錄裡);④ 全 repo 交叉搜尋確認殘留樣式 0 命中
狀態 ✅ 已修(CM-2048,commit e8e134a4e)。歷史 commit 仍有舊值,因已換發不改寫歷史
§4

M15-6 🟡 同一份交接文件裡的三樣帳密

欄位 內容
嚴重度 🟡 中(總表 #73;STRIDE:竄改資料、權限提升)
在哪裡 AI 儀表板模組檢視範圍外另外撈到的(與儀表板功能無關):與 M17-10 同一份交接文件、同一段,裡面一共明文寫了三樣——內部套件倉庫的管理員帳密、檔案儲存服務的金鑰、一組測試客戶的畫面登入帳密
攻擊面位置 同 M17-10:主專案程式碼倉庫,任何讀得到程式碼的人都構成攻擊面,不需要系統帳號;三樣都只在公司內網有效,外人拿到用不上——這也是評為中而不是高的理由
信任邊界位置 三條邊界被同一份文件同時打穿:讀程式碼的人 ↔︎ 套件倉庫發布權、讀程式碼的人 ↔︎ 檔案儲存(稽核證據)、讀程式碼的人 ↔︎ 測試環境的登入畫面。三樣都該只在部署文件或各機器不進版控的設定裡
元件端點位置 外流處:docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md(套件倉庫帳密那一行、UPDATE public.system_configs 設定檔案儲存那一段的 minio_access_key/minio_secret_key、測試客戶 102 的登入帳號那兩處)。對應服務:套件倉庫 …/repository/pypi-group/simple(主專案 pyproject.toml)、檔案儲存的連線設定存在 system_configs 表
駭客怎麼打 ① 讀得到程式碼的人打開交接文件;② 拿套件倉庫管理員帳密推一個帶後門的元件,等下次打包裝進客戶手上的程式(與 M17-10 同一招);③ 拿檔案儲存金鑰直接讀寫存放的稽核證據,繞過系統畫面上所有權限檢查;④ 拿那組畫面帳密直接登入測試環境
得手什麼 往安裝包塞東西的位置、稽核證據的讀寫權、一個可直接登入的帳號
修正的做法 ① 三樣都換發,舊值失效(環境操作);② CM-2048 清掉交接文件裡三處明文,全部改寫成「請查 .env 或部署文件」,並重建網頁與文件站鏡像;③ 模組頁特別提醒的陷阱也一併收掉:同一組套件倉庫密碼還散在打包腳本與前端三支設定檔,只清那四個檔、漏了這份交接文件等於沒清乾淨——那四個檔另由 M23-6 收;④ 「連套件倉庫沒加密」是傳輸問題(M23-5),這條是外流問題,分開修
狀態 ✅ 已修(CM-2048,commit e8e134a4e)。三樣已換發,交接文件殘留字串已清
§5

M23-6 🟡 打包腳本寫死套件倉庫帳密並進了版本控制

欄位 內容
嚴重度 🟡 中(總表 #75;STRIDE:竄改資料、資料外洩)
在哪裡 意見回饋模組檢視時往外查到的:打包前端、做出客戶要安裝的前端映像檔的那支腳本,以及前端程式碼倉庫的三支 CI 流程設定檔——四個檔都寫著同一組內部套件倉庫帳密,跨兩個程式碼倉庫
攻擊面位置 主專案與前端兩個程式碼倉庫,任何讀得到程式碼的人都構成攻擊面,不需要系統帳號。帳密不是明文,是一行指令就能還原的編碼——這不是加密,只是換個樣子寫。解出來是管理員層級的帳號,不是限定唯讀的專用帳號
信任邊界位置 「讀得到程式碼的人」↔︎「打包流程與套件倉庫」。帳密應該只在打包機本機(不進版控的認證檔)或 CI 平台的遮罩變數裡,建置時臨時掛進去、用完就消失;當時直接寫成腳本的預設值與 CI 設定檔的變數值,還以建置參數傳進去——建置參數會留在建置紀錄、快取與映像檔歷史裡,連映像檔本身都帶著它
元件端點位置 主專案 scripts/build/build_fe_image.sh(NEXUS_AUTH 預設值,自 8c503ae5a 起入版控,以 --build-arg NEXUS_AUTH 傳入);前端 repo Dockerfile(ARG NEXUS_AUTH + npm config set);前端 repo .gitlab-ci.yml、.gitlab-ci-on-premises.yml、.gitlab-ci-terraform.yml 的 variables 段。被拿來抓套件的倉庫:…/repository/npm-proxy/
駭客怎麼打 ① 任何能讀這兩個程式碼倉庫的人,打開打包腳本或 CI 設定檔;② 把那串編碼一行還原,得到套件倉庫的管理員帳密;③ 在內網登入套件倉庫,往前端會抓的套件來源放一個夾帶惡意程式的版本;④ 下次打包前端映像檔時,npm install 從同一個倉庫把它抓進來,跟著前端映像檔送到每個客戶的瀏覽器前;⑤ 另一條路:拿到任何一顆已出貨的前端映像檔,翻它的建置歷史一樣看得到這組帳密
得手什麼 往客戶安裝的前端程式裡塞東西的位置,以及公司套件倉庫的管理員權限
修正的做法 ① 前端打包改用建置當下臨時掛入的祕密檔(CM-2230):build_fe_image.sh 拿掉寫死的預設值,改成 --secret id=npmrc,src=${FE_NPMRC:-~/.npmrc},找不到認證檔就警告並改走公開套件來源,另加 --public-npm 選項;② 前端 Dockerfile 拿掉 ARG NEXUS_AUTH,改 RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=false,倉庫網址改用 npm install --registry 傳(不能再 npm config set,因為那個位置此時是唯讀掛入的祕密檔);③ CI 設定檔刪值(CM-2252):三支 CI 檔刪掉明文,改讀 GitLab 專案的遮罩變數,建置時先寫成暫存檔再用同樣的 --secret 掛入;④ 驗證:有/無祕密檔各完整建置一次都成功,兩顆映像檔的建置歷史與建置紀錄搜尋認證字樣皆 0 筆;三支 CI 檔搜尋認證字樣 0 命中
狀態 ✅ 已修,1.21.0 出貨(CM-2230:主專案 commit 237ffbf86、前端 commit 7e8f545;CM-2252:前端 commit 48cc2bd)。帳密已輪換,歷史 commit 仍含舊值、不改寫歷史
§6

M23-5 🟡 打包機抓套件走沒加密的連線、又不比對指紋

欄位 內容
嚴重度 🟡 中(總表 #101;STRIDE:竄改資料)
在哪裡 意見回饋模組檢視時撞到、後來證實是全面性的:所有程式庫連公司內部套件倉庫都走沒有加密的連線,而這是安裝套件的主要來源——28 支程式庫全部一樣(現役套件 21、已封存 6、主專案 1)。同時,用來比對套件有沒有被掉包的「版本鎖定檔」在套件那邊沒進版本控制
攻擊面位置 公司內網。攻擊者要能在打包機與套件倉庫之間的網路上動手腳(同網段的人、或一台被入侵的內網機器),不需要任何系統帳號。這一面是設計上就存在的:倉庫網址本來就寫成沒加密的連線
信任邊界位置 打包機 ↔︎ 套件倉庫之間的網路。打包機收到套件時,應該在安裝前確認「這就是當初鎖定的那一份」——靠加密連線或靠比對指紋(雜湊值)至少一道;當時連線沒加密、套件那邊的鎖定檔又沒進版控,兩道都沒有,收到什麼就裝什麼
元件端點位置 主專案 pyproject.toml 的 [[tool.poetry.source]] nexus(http://…/repository/pypi-group/simple,其餘 27 支程式庫同形);打包機安裝相依的那一步:scripts/build/build_release.sh 的 step_deps(poetry install --no-interaction --without dev,吃 poetry.lock);鎖定檔被排除的位置:套件 monorepo 根目錄 .gitignore 的 poetry.lock 規則
駭客怎麼打 ① 在公司內網找一個位置,把打包機往套件倉庫的流量導到自己這裡;② 等打包機開始裝套件;③ 回一個被改過的套件檔——因為連線沒加密,打包機分不出真假;④ 套件安裝時本來就會執行它自帶的程式碼,於是攻擊者的程式碼直接在打包機上跑;⑤ 打包機產出的就是客戶實際拿到的安裝檔,惡意內容隨之出貨
得手什麼 在打包機上執行任意程式碼,並把它帶進客戶的安裝檔
修正的做法 ① 鎖定檔全部入版控:主專案自 2026-08-15 起入版控(commit 02146e9db);套件 monorepo 拿掉根目錄 .gitignore 擋 poetry.lock 的規則,當時已產生鎖定檔的 17 支套件一併入版控(commit 09cbb7cf);② 為什麼這樣就擋得住掉包:鎖定檔裡每個套件檔都附指紋(例如主專案 poetry.lock 裡每個內部套件的安裝檔都有 sha256),打包機 poetry install 照鎖定檔裝,下載內容被換就會因指紋不符而安裝失敗;③ 連線維持沒加密:決策者裁定不改,理由是倉庫與打包機都在公司內網,掉包已由指紋擋住;④ 這道防線擋的是「同一個版本被換內容」,擋不了「有發布權的人推一個新版本」——那是 M17-10/M15-6/M23-6 的範圍,靠帳密不外流守住
狀態 ✅ 已修(版本鎖定檔入版控:主專案 02146e9db、套件 monorepo 09cbb7cf,1.21.0 出貨;CM-2067)。連線改加密一項🚫 裁定不修(決策者裁定,內網)
§7

M07-12 ⚪ 證據分類程式的外部套件沒鎖版本也沒校驗碼

欄位 內容
嚴重度 ⚪ 低(總表 #132;STRIDE:竄改資料)
在哪裡 證據自動分類模組:分類程式的容器映像檔用到六個外部套件——兩家 AI 服務的程式庫,加上處理 Word、Excel、PowerPoint、PDF 的程式庫——全部寫成「某版以上」,沒有校驗碼
攻擊面位置 公開的套件來源與上游維護者。這是唯一一條攻擊者完全不必碰我們系統就能得手的——只要上游某個套件被掉包或維護者帳號被盜,不需要我們任何帳號、也不需要進公司內網
信任邊界位置 外部套件來源 ↔︎ 我們的映像檔建置。建置時應該只接受「當初審過、指紋相符」的那一份;當時寫「某版以上」,等於把「這次要裝哪一版、內容是什麼」的決定權交給上游,每次重建裝進去的都可能不一樣
元件端點位置 套件 monorepo jedi-evidence-classification/docker/Dockerfile(當時 pip install -r requirements.txt 不驗指紋)與 docker/requirements.txt(六行 >=);主專案建置入口 scripts/build/build_classifier_image.sh(用同一支 Dockerfile)
駭客怎麼打 ① 攻擊者盜用某個上游套件(例如處理 PDF 的那個)的維護者帳號,或在某個套件鏡像站動手腳;② 發一個在「某版以上」範圍內的新版本,裡面夾帶惡意程式;③ 我們下一次重建分類映像檔時,安裝程式自動抓最新那版裝進去,沒有任何比對;④ 惡意程式跟著分類容器上線,每一份送進來分類的客戶證據檔都經過它手上
得手什麼 在分類容器裡執行任意程式碼,碰得到所有送去分類的客戶證據檔
修正的做法 ① 整串相依全部鎖死:原本的六行頂層宣告搬到 requirements.in 原樣保留,新的 requirements.txt 由 pip-compile --generate-hashes 產生——連同間接用到的共 24 支全部鎖成固定版號、每支附 sha256(共 542 個);② 建置時強制比對:Dockerfile 改 pip install --require-hashes --no-deps,少一個指紋或指紋不符就整個建不起來;--no-deps 是不讓安裝程式自己補裝沒鎖到的套件,裝完再跑 pip check;安裝程式 pip 本身也鎖版 24.0;③ 升級有固定路徑:新增 relock.sh,在與出貨機同平台(linux/amd64、python:3.11-slim)的容器裡重產鎖定檔,rebuild.sh --relock 轉呼叫它;④ 驗證有牙齒:故意把其中一個套件的指紋改一碼後建置,會在指紋不符處失敗;⑤ 後續同一條線又把映像檔的基底映像也釘死指紋(CM-2229,套件 commit 1189307a)
狀態 ✅ 已修,1.21.0 出貨(CM-2226,套件 commit 24e40b84)。現況:鎖定檔後續已重產過(CM-2352/CM-2354),仍是全鎖版號+指紋;自家的 AI 閘道套件是建置時現場打包、另行安裝,不在指紋鎖定範圍內