---
title: A03 軟體供應鏈失效
eyebrow: Guidant AI 資安檢視總報告 · OWASP Top 10（2025）
h1: A03 軟體供應鏈失效（Software Supply Chain Failures）
lede: 命中這一類的 **4 件**，還原成模組頁的 **5 條原始條目**。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🟡 4", kind: accent }
  - { text: "⚪ 1", kind: plain }
  - { text: "已修 5", kind: ok }
---

## 這一類是什麼

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

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

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

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

> **為什麼是 5 條不是 4 件**：[總結](../SUMMARY.html)問題總表的 #73 是同一份交接文件、同一行帳密，被公告與 AI 儀表板兩個模組各撈到一次，總表併成一件；本頁拆回模組頁原始條目，兩條都列。標題括號內是總表編號，可回去對照。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M17-10 套件倉庫與檔案儲存的帳密寫在交接文件裡](#m17-10) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 2 | [M15-6 同一份交接文件裡的三樣帳密](#m15-6) | 🟡 中 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 3 | [M23-6 打包腳本寫死套件倉庫帳密並進了版本控制](#m23-6) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 4 | [M23-5 打包機抓套件走沒加密的連線、又不比對指紋](#m23-5) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 5 | [M07-12 證據分類程式的外部套件沒鎖版本也沒校驗碼](#m07-12) | ⚪ 低 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |

---

## M17-10 🟡 套件倉庫與檔案儲存的帳密寫在交接文件裡 {#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 仍有舊值，因已換發不改寫歷史 |

## M15-6 🟡 同一份交接文件裡的三樣帳密 {#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`）。三樣已換發，交接文件殘留字串已清 |

## M23-6 🟡 打包腳本寫死套件倉庫帳密並進了版本控制 {#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 仍含舊值、不改寫歷史 |

## M23-5 🟡 打包機抓套件走沒加密的連線、又不比對指紋 {#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）。連線改加密一項🚫 裁定不修（決策者裁定，內網） |

## M07-12 ⚪ 證據分類程式的外部套件沒鎖版本也沒校驗碼 {#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 閘道套件是建置時現場打包、另行安裝，不在指紋鎖定範圍內 |
