---
title: 檔案上傳下載
eyebrow: Guidant AI 資安檢視 · 模組報告
h1: 檔案上傳下載（jedi-file-upload）
lede: 產品裡所有附件與稽核證據檔的存取，都走這一塊。**這是高風險問題最多的一塊**——四道防線曾經一道都沒有，目前補了最底下那一道。
---

## 這塊在產品裡做什麼

客戶在產品裡上傳的每一份東西，都經過這一塊：稽核證據、佐證文件、報告附件、系統匯出的檔案。

::: grid2
::: {.card .plain}
#### 它平常在做什麼
1. 使用者**上傳**一份檔案，系統存起來並給它一個編號
2. 之後用那個編號**下載、預覽、轉檔成 PDF、刪除**
3. 檔案本身放在雲端儲存空間，資料庫只記「誰上傳的、叫什麼名字」
:::
::: {.card .crit}
#### 為什麼這塊特別要緊
存在這裡的是**稽核證據**。

稽核證據如果能被別人取走、竄改或刪除，**整份稽核報告的可信度就沒有了**——那正是這個產品存在的理由。
:::
:::

---

## 檢視軌跡

**檢視期間**：2026-09-11 ～ 2026-09-12｜**範圍**：71 個檔案｜**共 3 輪**

> **原始技術報告**放在需求中心的 [FR-086 站](https://guidantai-feature-doc.jedicotech.com/FR-086-2609-file-upload-security-scan/)，檔名見下表最後一欄。那裡有每一輪的完整技術細節與逐條推理，供需要深究或稽核抽查時查閱。

| 輪次 | 查什麼 | 檔數 | 覆核投票 | 結果 | 原始報告檔名 |
|---|---|---:|---|---|---|
| **B1** | 對外的功能入口——上傳、下載、預覽、刪除怎麼進來的 | 35 | 18 票全投完 | 找到 4 條（3 高 1 中）＋人工另查出 1 條 | `scan-B1-http-boundary` |
| **B2** | 我們主系統這端怎麼把它接上來 | 10 | 36 票全投完 | 範圍內 1 條高風險＋人工另查出 4 條 | `scan-B2-host-wiring` |
| **B1b** | 檔案實際存到哪裡、資料怎麼取 | 26 | 自動檢視零發現 | **人工找到 5 條**，並**直接查資料庫坐實了最後一道防線也沒有** | `scan-B1b-storage-backends` |

::: {.callout .warn}
**第三輪自動檢視「零發現」，但那一輪其實找到 5 條**

這一輪是很好的例子，說明為什麼不能只看工具的結果——**5 條全部是人工查出來的**，包括這塊證據最扎實的那一條（統籌者與執行者各自連進開發環境，用唯讀方式查資料庫證實）。

詳見[檢視方法與工具 → 為什麼不能只看 AI 的結果](GUIDE-01-method-and-tools.html#why-human)。
:::

::: {.callout .ok}
**另外，開工時列的懷疑有一條被推翻了**

第一輪的工作單上自己標「最重要」的那個懷疑，查證後**不成立**。報告裡把「是什麼擋住了它」寫清楚了——因為擋住它的如果是巧合，哪天有人整理程式碼把巧合改掉，問題就自己回來了。
:::

::: {.callout .warn}
**這一頁還做過第二次查證：講者逐條回程式碼核對，推翻了三條**

上面三輪是檢視當下的結果。這一頁定稿前，講者把每一條拿回程式碼與資料庫再對一次，結果**改掉了三條**：

| 原本寫 | 查證後 |
|---|---|
| 上傳沒有單檔大小上限 | **不成立**——後端有 50MB 上限，而且是請求一進來就擋 |
| 連儲存空間「設定沒填就走明文」 | **不是沒填**——七筆設定全都填了，而且全都填成不加密 |
| 借用別家帳密那條「要先做產品決策」 | **不是決策題**——那是修另一個錯誤時留下的過渡措施，忘了拿掉 |

這三條的完整說明在下面各自的段落裡。**把推翻的過程留在報告裡是刻意的**——一份只有「找到什麼」沒有「推翻什麼」的報告，反而不可信。
:::

---

## 問題一覽

**排序按風險，同風險的同一類排在一起。**「出事會怎樣」那欄講的是**業務影響**——誰受影響、損失什麼，不是技術現象。

> **編號跳過 10 是刻意的**：原本列了 11 條，第 10 條經查證不成立，說明在表格下方。其餘編號維持不動，方便跟先前的紀錄對照。

::: {.callout .warn}
**讀這張表之前要先知道一件事：產品還沒有出貨給任何真實客戶。**

下面每一條講的「客戶會怎樣」，都是**產品上線之後會發生的事**，不是已經發生過的事。目前系統裡的資料全部是我們自己的測試資料。

**這不會讓任何一條變得比較不嚴重**——問題都是真的，只是還沒有人受害。**這一輪檢視正是為了在出貨前把它們處理掉。**

（**一個例外**：表裡凡是講「我們自己的文件網站／程式碼倉庫／測試機」的條目，與產品出不出貨無關——那些曝光是真的發生過的，已在各條裡標明。）
:::

| # | 問題 | 風險 | 分類 | 出事會怎樣（誰受影響） | 怎麼修 | 狀態 |
|---|---|---|---|---|---|---|
| **1** | 拿到檔案編號就能下載別人的檔案 | 🟠 高 | 只驗登入、不檢查歸屬 | **同一家公司內任何有帳號的人，都能取走別部門的稽核證據**。資料庫那層補上之後，跨客戶已經擋住了，但**同一客戶內跨專案、跨部門仍然拿得到** | 取檔之前先查出這個檔掛在哪個業務物件上，再去問那個物件的權限（第 1、2、3、7 條同一套修法） | ✅ **已修（CM-2033）** |
| **2** | 能替任何檔案換發一張免登入的下載通行證 | 🟠 高 | 只驗登入、不檢查歸屬 | 通行證的機制本身是紮實的（我們自己簽、只有 120 秒、綁死一個檔案與一種用途、改內容就失效），**漏掉的只有一問：換發的時候不問這個檔是不是你的**。所以同公司內任何有帳號的人，拿別部門的檔案編號也換得到 | 換發前補上同一道歸屬檢查（與第 1、3、7 條同一套） | ✅ **已修（CM-2033）** |
| **3** | 取檔案的那層只憑編號就交出檔案 | 🟠 高 | 只驗登入、不檢查歸屬 | 與第 1 條是同一件事的另一半。**這一層是已經把檔案找出來、最有條件去問「這是誰的」的地方**，該補檢查的就是這裡 | 同第 1 條，在這一層補上歸屬檢查 | ✅ **已修（CM-2033）** |
| **4** | 上傳的網頁檔，別人點預覽時會在我們的網域裡被當成程式執行 | 🟠 高 | 外部送什麼就收什麼 | **受害者只要點一下預覽，攻擊者的程式就在受害者的瀏覽器裡、用受害者的身分跑起來**——可以偷走他的登入狀態、冒用他的身分操作。上傳者只需要一個有效帳號 | 預覽端建可預覽類型清單、清單外一律當附件下載；清單內的加上瀏覽器層的隔離指令；並由伺服器自己確認檔案內容真的是那個格式。實際做法：預覽改成白名單＋驗檔頭，HTML 加 CSP sandbox，不符合就強制下載 | ✅ **已修（CM-2062）** |
| **5** | 任何登入者打一支查設定的功能，就拿到雲端儲存的帳號密碼明文 | 🟠 高 | 密碼外流 | **拿到那組帳密後可以完全繞過整個系統、直接連進儲存空間**——之後在程式裡補再多檢查都沒用。拿到的是自己那家公司的那一組，但**落地版是一客戶一套系統，那一組就是那家的全部** | 設定類的密碼欄位一律預設不回傳；補權限檢查；並清掉寫死在程式與腳本裡的那組帳密。**五個步驟有先後順序**，見下方 | ✅ **已修（CM-2063）** |
| **6** | 存放檔案紀錄的資料表沒有做客戶隔離 | 🟠 高 | 客戶資料沒隔開 | 這是「四道防線都沒有」的最後一道。**資料庫本身也擋不住跨客戶存取** | 加客戶歸屬欄位並開啟隔離 | ✅ **已修**（2026-09-16，2,718 筆全數補上客戶歸屬） |
| **7** | 任何登入者可以永久刪除任何檔案 | 🟠 高 | 只驗登入、不檢查歸屬 | **別家客戶的稽核證據會被永久刪除**，而且沒有救回的機制。**對稽核產品來說，證據被取走還做得下去，證據被刪就做不下去了**，而且不可逆 | 同第 1 條的歸屬檢查，但要問的是「能不能刪」而不是「能不能看」；並改成可復原的刪除 | ✅ **已修（CM-2033）** |
| **8** | 讀不到自己的儲存設定時，系統會去借用別家客戶的帳密 | 🟡 中 | 客戶資料沒隔開 | 系統會繞過隔離把所有客戶的設定撈出來、取第一筆用，**只留一筆警告就繼續跑**。實際上借到的是**最早建立的那家客戶**的儲存空間——檔案會存進別人家，或去別人家找檔案 | 先把「撈全部取第一筆」改成「只取系統層那一列」（一行改動，風險當場消失）；等錯位檔搬完再整段拿掉 | ⬜ **未修** |
| **9** | 本機儲存刪檔時，漏清了轉檔產生的備份 | 🟡 中 | 資料清理不完整 | **使用者以為刪掉了，但轉出來的 PDF 還留在硬碟上**——「刪了沒刪乾淨」對稽核產品是實質缺陷 | 補上那一步；並把兩種儲存方式的共通邏輯抽到同一處。實際做法：衍生檔清理抽成共用函式，本機儲存模式刪檔時一併清掉快取 PDF | ✅ **已修（CM-2062）** |
| **12** | 原廠共用的檔案（例如範本），一般公司的登入帳號按刪除，硬碟上的檔案本體會先被刪掉，資料庫紀錄卻刪不掉 | 🟡 中 | 只驗登入、不檢查歸屬 | 刪除時是**先刪檔案本體、再刪資料庫紀錄**；資料庫那一步會因為不是原廠而被擋下，但檔案本體已經沒了，畫面還回「成功」。結果是**原廠給所有客戶用的範本檔全部打不開，所有客戶一起受害** | 兩步：① 一般公司刪原廠共用檔直接拒絕；② 刪除順序改成先刪紀錄、成功才刪檔案本體 | ⚠️ **部分修**——第 ① 步已擋住（刪除入口補了歸屬檢查、原廠共用檔一律不准刪，1.21.0 出貨）；第 ② 步刪除順序還沒改，也還沒排卡。這條是主系統掃描查出的 |
| **11** | 連雲端儲存時不使用加密連線 | ⚪ 低 | 密碼外流 | 檔案內容與帳密在公司內部網路上以明文傳輸。**目前七筆設定全部都明確填成不加密**，不是漏填 | 出貨裝機時預設開啟加密；現有環境另外評估切換時機 | 🚫 **裁定不修（CM-2063，決策者 2026-09-23）** |

### 原第 10 條：查證後不成立 {#item10}

原本寫「上傳沒有單檔大小上限、也沒有數量上限」。**前半句是錯的。**

| | |
|---|---|
| **實際狀況** | 後端有 **50MB 上限**，而且是**請求一進來就擋**，不是讓人傳完才發現 |
| **設定在哪** | `MAX_REQUEST_BODY_MB`（設在 `config/config.py`，由 `core/app_factory.py` 套用，請求一進來就提前擋下） |
| **前端另有一道** | 各個上傳元件自己也有限制，10MB～50MB 不等 |
| **最外層那道確實沒擋** | 前端派送程式的設定裡沒設上限，但**設定檔註解明寫是刻意交給後端把關**，不是疏漏 |

**屬實的只有「數量沒有上限」這半句**——但整包請求受 50MB 頂著，就算一次塞很多檔，總和一樣被擋在門外。

所以這一條**從「缺陷」改判為「不成立」**；「數量要不要另外訂上限」降為**改善建議**，不列為資安問題（訂多少是業務問題，見最後的待決策清單）。

---

## 這塊最重要的一件事：四道防線 {#four-layers}

::: {.callout .crit}
**一個檔案要被不該看的人拿走，本來應該有四道關卡擋著。這塊曾經四道都沒有。**
:::

用「有人拿著一個檔案編號來要檔案」來看這四道：

| 第幾道 | 這道關卡應該問什麼 | 原本的狀況 | 現在 |
|---|---|---|---|
| **一、入口** | 你有沒有登入？**這個檔案是你的嗎？** | 只問了前半句 | ⬜ 未改 |
| **二、業務邏輯** | 這個編號對應的檔案，該不該給你？ | **完全沒問** | ⬜ 未改 |
| **三、取資料** | 查詢時有沒有限定只能查自己的？ | **沒有任何範圍限制** | ⬜ 未改 |
| **四、資料庫** | 就算前面都漏了，資料庫本身擋不擋？ | **表裡連「這是哪家客戶的」欄位都沒有** | ✅ **已補** |

### 第四道是怎麼補上的——以及為什麼這件事值得注意

2026-09-16 那次改動，把客戶歸屬欄位加進資料表並開啟隔離，2,718 筆資料全部補上歸屬。

**但那次改動不是為了修這個問題。** 它是另一項功能開發順手做的——那項功能要拿這張表當暫存區，才需要加上這些欄位。

::: {.callout .warn}
**這帶出一個值得記住的現象**

我們在複查時發現：**這段期間補起來的都是資料庫那一層，應用程式那一層幾乎沒有動過。**

「只驗登入、不檢查歸屬」那一整組問題（橫跨七個模組、十幾條）**一條都沒有被碰過**——而那正是我們自己標記為「最該優先處理」的那一組。

所以現在的狀態是：**資料庫那道牆補上了，應用程式那道門還是開著的。** 跨客戶被擋住了，但同一家客戶內跨專案、跨部門，仍然拿得到。
:::

---

::: {.callout .plain}
**下面只展開需要你自己判斷、或容易被誤解的幾條。** 其餘的修法明確，看上面表格的「怎麼修」那一欄就夠了——**沒有展開不代表沒查，也不代表不重要。**
:::

## 第 1、2、3、7 條：同一件事的四個出口 {#ownership}

::: {.callout .crit}
**這四條不是四個問題，是同一個問題的四個出口**：下載、換發通行證、取檔、刪除。

**所以它們是一套修法，不是四套。** 分開修會變成同一段判斷抄四遍，而且一定有一個出口會漏掉。
:::

### 病因一句話

**檔案模組不知道自己保管的檔案屬於什麼，也從來沒有去問過知道的人。**

它做的事只有一行——拿編號去撈，撈到就送出。

這點很重要，因為它決定了修法方向：**這不是檢查寫錯、也不是規則不夠細，是「去問一下」這個步驟從來沒有被設計進去。**

::: {.callout .warn}
**先排除一個直覺但是錯的修法：用「誰上傳的」來判斷**

初步的想法是「取檔案前先比對這筆資料的主人」。**這個方向是錯的**，因為「主人」如果指的是上傳者，會同時擋錯人、也放錯人：

| 會擋錯的 | 會放錯的 |
|---|---|
| 同專案的稽核員看不到證據——**他不是上傳的那個人** | 上傳者被調離專案之後，**還是看得到** |
| 上傳者離職，檔案變成沒人能開的孤兒 | |

正確的問法不是「誰上傳的」，是「**這個檔掛在哪件事情上，那件事你有份嗎**」。上傳者是誰仍然要記，但那是留紀錄用的，不能當權限判準。
:::

### 四個出口的差別只有一個：問的是哪一種權限

| 出口 | 要問什麼 |
|---|---|
| 下載、預覽（第 1 條） | 這個檔掛著的那件事，**你看得到嗎** |
| 換發通行證（第 2 條） | 同上——通行證只是換一種拿法，門檻不該比較低 |
| 取檔那一層（第 3 條） | 同上，而且**這一層是最該補的地方**（見下） |
| 刪除（第 7 條） | 這個檔掛著的那件事，**你改得動嗎** |

**前三個問「看得到嗎」，刪除要問「改得動嗎」。** 同一個專案裡，能看證據的人通常比能刪證據的人多很多——**刪除如果照抄下載的判斷，等於把刪除權放給所有看得到的人**。

### 修法要動多少：歸屬資料一直都在，只是沒人去問

::: {.callout .warn}
**這裡要更正一個先前講錯的說法**

先前說「歸屬欄位已經在表裡了，只是沒拿來用」——**這句不實**。檔案那張表上**沒有**「掛在哪個業務物件」的欄位：

| 欄位 | 實際是什麼 |
|---|---|
| 那個看起來像關聯的欄位（`ref_id`） | 是「轉檔產生的 PDF 指回原始檔」的自我參照，**全庫 2,821 筆裡只有 2 筆有值** |
| 上傳者欄位（`owner_user_id`） | **96% 是空的**，而且目前沒有任何規則在用它 |

**真正的歸屬關係在下游**——是那些表反過來指著檔案：

| 哪張表指著檔案 | 代表什麼檔 |
|---|---|
| `job_evidences.file_id` | 稽核證據 |
| `ssp_reference_documents.file_id` | 安全計畫的佐證文件 |
| `issue_upload_file_mapping.file_uid` | 意見單的附件 |

**這個更正對成本判斷很重要**：歸屬資料一直都在，**不需要新建欄位、不需要回填舊資料**——只是從來沒有人去問過它們。
:::

### 修法的形狀：一個共用通道 ＋ 一張登記表 {#dispatch}

一個很自然的顧慮是：**「難道要為每一種檔案各開一支下載功能、各做一套權限檢查？」**

**不用。** 形狀是這樣：

| 誰 | 做什麼 |
|---|---|
| **檔案模組** | 留一個空位，說「我需要一段能回答『這個人能不能拿這個檔』的程式」 |
| **各業務模組** | 把自己的那段回答程式**登記**進來 |
| **要取檔的時候** | 檔案模組先查出這個檔屬於哪一類，再去呼叫對應的那一段 |

**檔案模組不懂任何業務**——它只做三件事：問、等答案、照做。

各業務模組的回答程式只回答兩件事：

1. **這個檔是不是我的**（查自己那張關聯表）
2. **如果是，這個人能不能看**（用自己既有的權限規則）

第二題各業務模組本來就會算——**只是從來沒有人來問過它**。

::: {.callout .decided}
**那份「登記」是程式裡的一張對應表，不是資料庫的表**

大約十幾列。做成資料庫的表會出問題，因為**規則是活的邏輯**（要查成員、看角色、算狀態），做成資料表等於用資料庫寫程式。

**擴充成本**：新增一種檔案用途時只加一列登記，檔案模組零改動；**資料庫零改動、舊資料零回填**。
:::

**有三條規則要定死，否則這套修法會有破口**：

| 規則 | 為什麼 |
|---|---|
| ① 查不到登記的類型 → **拒絕**（不是放行） | 漏登記時往「看不到」倒，不是往「全都看得到」倒 |
| ② 沒有任何模組認領的檔 → **拒絕** | 孤兒檔不該因為沒人管就人人可取 |
| ③ 拒絕時回「**找不到這個檔案**」而不是「你沒有權限」 | 後者等於告訴對方「這個編號是有效的」，可以拿來一個一個試 |

這個形狀**與產品既有的把關方式是一致的**：「只看你是誰」的檢查掛在入口，「要先找出是什麼東西才知道該問誰」的檢查放在業務邏輯裡。檔案權限屬於後者，**是目前唯一沒接上的一塊**。

### 第 2 條的補充：通行證的機制本身是好的

產品有個功能：可以替一個檔案換發一張「通行證連結」，讓瀏覽器直接開啟檔案不用再登入一次。這個設計本身是合理的，**而且做得相當紮實**：

| 這張票 | 實際狀況 |
|---|---|
| 誰簽的 | **我們自己簽的**，不是雲端儲存商的預簽網址 |
| 有效多久 | **120 秒** |
| 能開什麼 | **綁死一個檔案、一種用途**，換不了別的 |
| 能不能偽造 | 有簽章，**內容改一個字就失效** |

**漏掉的只有一問：換發的時候不檢查這個檔是不是你的。** 只要有帳號，拿任意檔案編號都換得到——所以它跟第 1、3、7 條是同一個病，補的是同一道檢查。

::: {.callout .warn}
**這裡刪掉了一個先前的論述：「稽核證據可以被合法帶出公司」**

先前把第 2 條評得比第 1 條更嚴重，依據是「換出來的連結可以傳給公司外部的人」。

**這個依據不成立。** 一個人只要下載得到檔案，本來就能轉寄、存檔、拍照——任何系統都擋不住。**票能不能給外人開，對「檔案會不會外流」沒有增加任何風險。** 留著這句話反而給人挑毛病的把柄。

**「那第 2 條要不要降級」——已裁定：不再單獨排級。**

決策者 2026-09-20 裁定：第 2 條與第 1、3、7 條是**同一個洞的第四個出口**（都是「換發／取檔／下載／刪除時不問這個檔是不是你的」），所以**不單獨討論它的等級**——補上那一套歸屬檢查，四條一起消掉。風險等級一律以工單上的登記為準。
:::

### 第 7 條為什麼從「中」提到與前三條同級

原本評中的依據是「這是破壞不是竊取」。**這個依據對一般系統成立，對稽核產品不成立**：

| | |
|---|---|
| **證據被取走** | 難看，但稽核**還做得下去** |
| **證據被刪除** | 稽核**做不下去了**，而且**不可逆**——沒有救回的機制 |

而且查證時發現：**刪除的防護比下載還薄。** 下載與預覽至少接受兩條路（登入憑證或短效票），**刪除只驗登入**，連票那條路都沒接上，業務層也沒有任何判斷。

---

## 第 4 條：上傳的網頁檔在預覽時被當成程式執行 {#preview-xss}

### 先澄清一個常見的誤解

**伺服器不會執行任何上傳上來的檔案。** 整套件查證過，唯一會叫外部程式的地方是把文件轉成 PDF 的那一步，而且參數是寫死的，塞不進別的東西。

這條講的是另一件事：**別人點開預覽時，攻擊者的腳本在「他的」瀏覽器裡、用「他的」身分跑起來**——可以偷走他的登入狀態、冒用他的身分操作系統功能。

### 最有說服力的一點：同一個模組，兩個出口做法不同

| 出口 | 做法 | 結果 |
|---|---|---|
| **下載** | 一律當附件丟出去 | ✅ 安全 |
| **預覽** | **照使用者自己填的副檔名**決定格式，而且允許瀏覽器直接把它畫出來 | ⚠️ 有問題 |

**同一個模組、同一份檔案，兩條路做法不一樣。** 這說明不是「不知道要做」，是**預覽那一條漏掉了**。

### 怎麼修：保留可預覽，同時關掉風險 {#preview-fix}

::: {.callout .decided}
**要求是：HTML 報告要能繼續預覽。** 所以不能用「乾脆全部不給預覽」這種解法。

定案的做法是三件事一起做：

| | 做什麼 | 解決什麼 |
|---|---|---|
| **一** | 預覽端建一份**可預覽的類型清單**，清單外一律當附件下載 | 把可預覽的範圍縮到可控 |
| **二** | 清單內的，回應時**加上瀏覽器層的隔離指令**——瀏覽器會禁止這個頁面執行腳本、也不讓它碰我們網域的任何東西，**排版與圖表照常看得到** | 就算清單內混進惡意內容也跑不起來 |
| **三** | **不只看副檔名**，由伺服器自己確認檔案內容真的是那個格式 | 目前副檔名是直接拿使用者上傳的檔名，完全沒有驗證 |
:::

**被排除的兩個選項，以及理由**：

| 選項 | 為什麼不採 |
|---|---|
| 轉成 PDF 再預覽 | HTML 轉 PDF 常跑版、每次預覽都要等轉檔，而且**只解決 HTML**，不涵蓋其他格式 |
| 把上傳的檔案放到另一個網域 | 落地版是客戶各自裝機，**多一個網域就是裝機文件上多一條**——某個客戶漏做就整個失效 |

**隔離指令之所以是首選**：一行就設定完、涵蓋所有類型（包含以後才冒出來的新格式）、不需要額外建設任何東西，而且是瀏覽器本來就支援的標準做法。

---

## 第 5 條：一支查詢就拿到儲存空間的帳密 {#storage-cred}

::: {.callout .crit}
**這條的特別之處：它讓其他所有修補都失去意義。**

拿到雲端儲存的帳號密碼之後，**可以完全繞過我們的系統、直接連進儲存空間**——那時候不管應用程式裡補了多少檢查，都沒有用。
:::

三層防護同時失效：

| 第幾層 | 本來應該做什麼 | 實際狀況 |
|---|---|---|
| 一 | 用來遮蓋機密的名單，要收錄儲存設定 | **沒有收錄** |
| 二 | 要遮蓋的欄位名清單，要包含帳密欄位 | **沒有包含實際用的欄位名** |
| 三 | 查詢設定的功能要檢查權限 | **只檢查有沒有登入** |

::: {.callout .warn}
**這一條的證據等級要標清楚：是從程式碼推論成立，沒有現場實測過**

開發環境還沒有設定雲端儲存，那張設定表是空的，**在本機打不出症狀**。這跟客戶資料隔離那一條（實際撈到筆數、任何人都能重跑）是不同等級的證據。

在稽核場合，「我們推論成立但還沒實測」是可以接受的說法；**「被發現沒標」才不可接受**。

**影響範圍也要說清楚**：那張設定表本身有做客戶隔離，所以拿到的是**自己那家公司**的帳密，不是全部客戶的。但**落地版是一客戶一套系統，那家就是全部**——對主力出貨形態沒有減輕。
:::

### 修法定案：密碼類欄位預設不回傳 {#mask-by-default}

::: {.callout .decided}
**原則比原本的建議更強**：設定類的密碼欄位**一律不回傳給前端**，只有程式內部要用的時候才取得真值。

| | 做法 | 漏掉的時候會怎樣 |
|---|---|---|
| **原本的建議** | 把儲存設定補進「要遮罩」的名單 | **漏登記 ＝ 密碼外洩**。這次就是這樣發生的——兩份名單都漏了儲存設定 |
| **定案的做法** | **反過來**：預設全部蓋掉，明確標記可以公開的才給 | **漏標記 ＝ 頂多前端少看到一個欄位**，前端會自己來反應 |

差別在於**漏掉的時候往哪一邊倒**。這個定法也順帶取消了「要不要盤查其他設定組有沒有漏登記」這個工作——**預設就是蓋的，不用盤**。
:::

### 施工順序：五步，順序不能換 {#five-steps}

::: {.callout .crit}
**所有修法都要先確定「功能不會壞」。這一條的順序如果做錯，會把客戶的密碼洗掉。**
:::

| 步驟 | 做什麼 | 為什麼不能往後排 |
|---|---|---|
| **一** | **後端先加防護網**：收到空值就保留原值、不要覆蓋 | **這一步必須第一個做**。有了它，後面任何一步出錯都不會造成密碼被清空 |
| **二** | 前端改成遮蔽顯示（`••••••••`），不填就保持原值 | 跳過這步直接做第三步的話，使用者改別的欄位順手存檔，**就會把密碼存成空的** |
| **三** | 後端改成不回傳密碼 | 前面兩道都到位了才動這裡 |
| **四** | **先查清楚有沒有背景工作在讀這組設定**（匯入、排程、轉檔常常用系統身分跑），確認後再補權限檢查 | 不查就補，可能把它們擋掉——**而且會悄悄失敗**，不會有人看到錯誤 |
| **五** | **盤點那組帳密還寫死在哪些地方**（環境變數、其他服務的設定檔、部署腳本），把它們收攏成單一來源 | 同一組帳密散落在很多處，**日後真要換的時候必然漏掉一處**，而症狀是「某個功能突然壞了」、不是明顯的錯誤訊息。趁出貨前收乾淨，成本最低 |

::: {.callout .ok}
**這一步不包含「換發密碼」，原因要講清楚**

先前這一步寫的是「盤點完之後換發那組帳密」，**前提是那組帳密已經外流**。查證後這個前提不成立：

- 這組是**隨產品出給客戶的**儲存空間帳密。
- **產品從未出貨給任何真實客戶**，這組帳密從頭到尾沒有離開過我們。
- 所以**沒有發生過外流**，「換發」要解的那個問題並不存在。

**但盤點本身仍然要做**，理由換成另一個：**帳密不該寫死在程式與腳本裡**。這是出貨前該收乾淨的衛生問題，不是外洩應變。

**要講清楚的是**：這不表示第 5 條不嚴重。**那個漏洞是真的**——只要產品一出貨、客戶一填進自己的儲存帳密，任何有帳號的人就拿得到。差別只在於**目前還沒有真實客戶受害**，所以現在修的成本最低。
:::

---

## 第 8 條：讀不到自己的儲存設定時，會借到別家客戶的 {#storage-fallback}

::: {.callout .warn}
**這條先前寫成「要先做產品決策：客戶之間共用儲存空間是不是刻意的設計」——那個判斷是錯的。**

查證結果：它不是產品決策題。它是 2026-07-29 修另一個錯誤（背景工作把檔案存錯客戶）時加上的**讀取端過渡措施**，當時的紀錄原話是「搬家完成前與未來零星錯位的讀取保底」。

**那次已經把寫入端治本了**（背景工作改成用該工作所屬的客戶去解析），這一段只是讓**已經存錯位置的舊檔還讀得到**。搬家從來沒做，所以它留到現在。
:::

### 而且它借到的不是「系統預設」

系統層的設定**確實存在**（有一列是給系統用的）。但正規取系統預設的程式都會明確指定要那一列，**只有這一段沒有任何條件**——它是把全部撈出來、照編號排序取第一筆。

實際資料上，系統那一列的編號排在業務客戶後面，**所以它永遠撈不到系統那列**。實際借到的是**最早建立的那家客戶**的儲存空間。

### 現在會走到這段的三種情況

| | 情況 |
|---|---|
| ① | **客戶完全沒有儲存設定**——目前開發環境裡有 3 個測試客戶是這樣（**都是我們自己建的測試資料，不是真實客戶**） |
| ② | 客戶**換過儲存後端**，舊檔還記著舊的型別 |
| ③ | 當初**那批存錯位置的舊檔** |

::: {.callout .warn}
**一個典型的「驗收環境打折掩蓋缺口」**

開發環境的那幾筆儲存設定，**位址、空間名、帳號完全相同**——所以借到誰的都看不出差別，測起來一切正常。

**要等客戶各自接上自己的儲存空間，這個問題才會現形。**
:::

### 處置定案：先一行止血，再列為待拿掉 {#item8-fix}

::: {.callout .decided}
| | 做什麼 | 效果 |
|---|---|---|
| **先做** | 把「撈全部取第一筆」改成「**只取系統層那一列**」 | **一行改動**，立刻從「借別家客戶的設定」變成「用系統預設」，**風險當場消失** |
| **再做** | 記錄成**已知的過渡措施**，等錯位檔搬家完成後**整段拿掉** | 治本 |

**被排除的選項**：先把搬家做完再拿掉這條退路——那是治本沒錯，但**卡在搬家工具還沒做**（工單 CM-1279），等它等不到。
:::

**另外兩件順手要做的**：

| | |
|---|---|
| **補三個測試客戶的設定** | 開發環境有三個測試客戶完全沒有儲存設定，原因是自動複製的機制（CM-1282）**只對之後新建的客戶生效**，先前建的沒有回頭補。順手補一次即可 |
| **把那行警告升級** | 目前出事時只有一行警告混在一般日誌裡，**沒有稽核事件、沒有告警**。對稽核產品來說，「這個檔案到底存在誰的空間」是必須答得出來的問題 |

---

## 第 11 條：連儲存空間時不加密 {#no-tls}

::: {.callout .warn}
**先前寫「設定沒特別填的話會走明文」——這個措辭不準確，而且差別很重要。**

實際查開發環境的資料庫：**七筆儲存設定全部都填了，而且全部填成不加密。**

| 先前的寫法 | 實際狀況 |
|---|---|
| 大家忘了填（**疏忽**） | **目前的設定就是不加密**（**現況**） |

差別在修法：**不能只改預設值**——現有環境都已經明確填了「不加密」，改預設對它們完全無效。
:::

### 要澄清：這條與 Google Drive 無關

這條講的是**我們自己裝在客戶機房的物件儲存**（MinIO／SeaweedFS）那條連線。

Google Drive 走的是 Google 官方工具，底層固定加密，**連「要不要加密」這個開關都沒有**。目前的寫法很容易讓人誤會成 Drive 那邊也有問題。

### 怎麼修

| | |
|---|---|
| **出貨裝機時** | 預設開啟加密 |
| **現有環境** | 另外評估切換時機（要確認兩端都支援、切換當下會不會斷線） |

---

## 仍待決策的三件事 {#pending}

::: {.callout .pending}
**這些本頁不下結論，列出來等決策。**

| | 待決的事 | 目前手上的判斷依據 |
|---|---|---|
| **一** | **第 5 條在全案的優先順序** | 分析上建議排在第 1、2、3、7 條之前或同一批——理由是它**修起來最便宜**，而且不修的話前面那幾條的修補**可以被繞過**（拿到儲存帳密的人根本不走我們的門） |
| **二** | **「系統共用」檔案的定義與授權規則** | 目前標成系統共用的那些檔在隔離規則裡是跨客戶全開的，開發環境裡有 14 筆這樣的資料（**都是測試資料**）。要定的是規則本身：「哪些檔案有資格標成系統共用、誰能標」——**出貨前定下來，才不會讓真實客戶的檔案落進這個狀態**。待決策者與 PM |
| **三** | **檔案數量上限訂多少** | 這是業務問題——證據檔平常多大、有沒有客戶會傳影片或整包日誌。待決策者與 PM |
:::

### 這一輪結掉的兩件

::: {.callout .decided}
**原本列在這裡待決、2026-09-20 已裁定的兩件，記錄在此供稽核對照——不是憑空消失。**

| 原本待決的事 | 裁定結果 |
|---|---|
| **第 2 條要不要降級** | **不再單獨排級。** 它與第 1、3、7 條是同一個洞的第四個出口，補上同一套歸屬檢查四條一起消掉，不需要單獨討論它的等級。原本把它排在第 1 條之上的依據（「可以帶出公司」）先前已判定不成立、[說明在上方](#ownership) |
| **換發儲存帳密的時機** | **結案：不需要換發。** 那是**隨產品出給客戶的**儲存帳密，而**產品從未出貨給任何真實客戶**——這組帳密從頭到尾沒有離開過我們，沒有發生過外流，「換發」要解的問題不存在。原本第五步改為「把寫死在程式與腳本裡的帳密收攏成單一來源」，[說明在上方](#five-steps) |
:::

---

## 這塊的結論

::: {.callout .crit}
**一句話**：這是高風險問題最多的一塊，**而且它守的是稽核證據**——證據能被取走、竄改或刪除，整個產品的價值就受影響。

**建議**：

1. **第 1、2、3、7 條是同一件事的四個出口，一套修法涵蓋得到**——下載、換發通行證、取檔、刪除。病因是「檔案模組從來沒去問過這個檔屬於什麼」，修法是「先查出它掛在哪件事上，再問那件事的權限」。**歸屬資料一直都在，不需要新增欄位、不需要回填舊資料。**
2. **第 5 條建議與上面那批一起做**（是否更優先待決策）——因為不修它，前面補的檢查全部可以被繞過。**而且它的修法有五個步驟的先後順序，做錯會把密碼洗掉。**
3. **第 8 條不是產品決策題**，是修另一個錯誤時留下的過渡措施忘了拿掉。**先一行止血**（改成只取系統層那一列），等檔案搬家完成再整段拿掉。

**另外要提醒**：第 6 條雖然標示已修，但**它是別的功能開發順手做的，不是針對這個問題修的**——那項功能要拿這張表當暫存區才需要加欄位。這一個月補起來的**全是資料庫那一層，應用程式那一層一條都沒動過**。剩下三道防線都還開著，不要因為看到「已修」就認為這塊沒事了。

**最後要講清楚一件事**：產品目前**還沒有出貨給任何真實客戶**，所以上面每一條都**還沒有傷害到任何人**——現在看到的檔案與設定都是我們自己的測試資料。**但這不代表問題不嚴重。** 每一條都是真的，產品一上線就會是真實客戶的稽核證據在承擔。**這一輪檢視的目的，正是在那之前把它們處理掉**——現在修，是最便宜的時候。
:::

| 統計 | |
|---|---|
| 檢視輪數 | 3 輪、71 個檔案 |
| 原本列出 | 11 條 |
| 查證後成立 | **10 條**（1 條查證後不成立，見[上方說明](#item10)） |
| 高風險 | 7 條 |
| 中風險 | 2 條 |
| 低風險 | 1 條 |
| 已修 | 1 條（資料庫層） |
| 部分修 | 2 條 |
| 未修 | 7 條 |
| 仍待決策 | 3 件（見[上方](#pending)） |
| 主系統掃描另查出 | 1 條（第 12 條，中風險，部分修） |

---

| 你想知道 | 看哪裡 |
|---|---|
| 我們用什麼方法查、結果有多可信 | [檢視方法與工具](GUIDE-01-method-and-tools.html) |
| 其他模組的檢視結果 | [回總報告首頁](index.html) |
