FR-086 · 需求索引 · 本頁由 build 掃資料夾生成

FR-086 檔案上傳下載(jedi-file-upload)資安檢查

✅ 三棒全部掃完並經首腦驗收(2026-09-12 arc 收口);發現已處理——高風險七條全數已修隨 1.21.0 出貨,M02-8 借用別家儲存設定、M02-11 雲端儲存未加密連線裁定不修,合計 14 條發現(HIGH 6/MEDIUM 4/LOW 4)。這條路四層全空已完整證實——網址入口只驗登入、業務邏輯零檢查、資料查詢無範圍條件、資料表無租戶欄位且隔離關閉(B1b 已用 DEV 資料庫實查坐實:relrowsecurity = f、policy 0 條、17 欄位無 tenant_id)。另有「任何登入者可取得本租戶物件儲存帳密明文」(B2-A,HIGH)。開卡五個疑點三成立兩推翻;路徑穿越兩側各證偽一次,但是執行順序意外擋住的、不是刻意防的。B1b 已驗收:首腦複核資料庫實查四個數字全對、三後端不一致與系統指令安全性逐項開檔核對屬實。

狀態:✅ 已完成 文件 3 份

🔴 一頁看完

B1 掃完了。結論:只要有一個能登入的帳號,再知道某個檔案的編號,就能把別家客戶的檔案下載走、線上預覽、永久刪除——系統從頭到尾只檢查「你有沒有登入」,從來沒檢查過「這個檔是不是你的」。

而且還能替別人的檔案換發一張免登入的下載通行證,直接把網址傳給公司外面的任何人。另有一條:上傳一個網頁檔,別人一點預覽就會在我們自己的網域裡執行。

四條全是同一個病:認得出你是誰,但不管你能碰什麼。四條共用同一道修法。

好消息:卡片列為「本棒最重要」的路徑穿越經查不成立——但它是靠執行順序意外擋住的,不是有人刻意防的,重構就會長回來。

B2 回答了 B1 留下的問題:宿主那半邊有沒有補上檢查?沒有。 合起來是一條四層全空的路徑——網址入口只驗登入、業務邏輯零檢查、資料查詢無範圍條件、資料表連租戶欄位都沒有(隔離也是關的)。沒有任何一層會問「這個檔是不是你的」。

B2 另外人工追出一條同級的:任何登入者打一支查設定的 GET,就拿到自己租戶物件儲存的位址、帳號、密碼明文——拿到後可完全繞過系統直接連上倉庫,應用層再怎麼補都管不到。三處小改可修。

B1b 補上最後一塊拼圖,而且證據等級最高:不是讀建表腳本推論,而是實際連上 DEV 資料庫查出來的——存檔案那張表的隔離開關是關的、隔離規則零條、17 個欄位裡連「這是哪家客戶的」都沒有。另查出主機硬碟後端刪檔時不清轉檔快取(物件儲存那邊會清),等於刪了沒真的刪掉。

📄 B1 報告|B2 報告|B1b 報告

§1

這在做什麼(白話)

產品裡所有「上傳檔案、下載檔案、預覽檔案」都經過這支套件。它決定三件事:

  • 使用者傳上來的檔案存到哪(主機硬碟、物件儲存、或客戶自己的設備)
  • 存成什麼檔名、放在哪個目錄
  • 下載時怎麼判斷**「這個檔是不是你的」**

這個 arc 要回答:有沒有辦法把檔案寫到不該寫的地方、有沒有辦法下載到別人的檔案、儲存憑證會不會外洩。

§2

為什麼掃它

  • 從未掃過。 跨 arc 總表把它列在建議順序第三,理由是「路徑穿越與任意檔案讀取的天然溫床」。
  • 存檔案的那張表沒有多租戶欄位(建表腳本檔頭自陳),意思是資料庫層對它沒有任何隔離兜底——應用層的歸屬檢查是唯一防線。
  • 首腦盤點看到的疑點密度高於前幾支(見下)。
§3

🔴 首腦盤點的五個現成疑點(未驗證,行號已開檔核對)

# 看到什麼 為什麼可疑
1 上傳時用使用者可控的字串拼出存檔路徑(api/routes/upload_file_route.py:101) 那兩個值來自請求內容,沒看到 ../ 過濾
2 任何登入者可替任意檔案編號換一張下載憑證(:150-156,只掛 @auth_required) 中間沒有「這個檔是不是你的」檢查
3 查檔案的資料層完全沒有範圍條件(infra/repository/upload_file_repo_impl.py:41-45) 加上那張表沒有租戶欄位,猜到編號就取得檔案
4 檔名處理很薄(common/utils/file_utils.py:16) 無副檔名白名單、無檔名正規化、無單檔大小上限
5 物件儲存憑證存在資料庫 JSONB,加密預設為關(app/upload_file/service/managed_file_upload_service.py:99-119) secure=v.get('secure', False)

第 1、2、3 條若都成立,合起來就是一條完整的跨租戶取檔路徑——這正是三棒要分別驗證的。

§4

怎麼切三棒(按業務功能垂直切)

棒 範圍 檔數 scanRoot 為什麼獨立一棒
B1 HTTP 邊界與契約 api/+app/+ports/+common/+plugin.py 35 jedi monorepo 上傳/下載/預覽/發憑證四條對外路徑同一條呼叫鏈,三個最直接的疑點都在這
B2 宿主接線 app/upload_file/+infra/upload_file/+core/upload_file_wiring.py+di_containers/upload_file/ 10 BE repo 檔數少但密度最高:用哪個後端、拿誰的憑證、算不算你的檔全在這
B1b 儲存後端與持久層 infra/+domain/+migrations/ 26 jedi monorepo 檔案真正被讀寫的地方;路徑穿越真正寫出界的動作在這裡

三棒是一條路徑的三段:B1 查「請求進來時路徑是不是使用者可控」→ B2 查「挑後端時有沒有驗歸屬」→ B1b 查「落地時有沒有兜底」。驗收時要互相對照。

🔴 砍掉了原本規劃的第四棒

偵察建議過一棒「remote_agent 檔案通道」,首腦查證後砍掉——它與 FR-077 的 CM-1594(R3 宿主接線,17 檔)範圍完全重疊。要掃那條路請派 CM-1594,不要在本 arc 重開。

檔數已核對(排名表過時)

跨 arc 總表的排名表寫「61 檔/2,510 行」,實際是 68 檔/2,877 行(排除測試與報告目錄後),低估約 11%。已據實際數字切棒。

§5

進度

棒 卡 檔 面板 發現 報告
B1 HTTP 邊界與契約 CM-1652 35 ✅ 18 票全投出,verified 4 條(HIGH 3/MEDIUM 1)+ 人工另記 1 條 LOW scan-B1-http-boundary.md
B2 宿主接線 CM-1653 10 ✅ 36 票全投、verified 範圍內 1H +人工 4 條(1H 1M 1L 1 記錄)
⚠️ 另 9 條範圍外全是舊案重複
報告
B1b 儲存後端與持久層 CM-1654 26 ✅ 6 票全投、verified
⚠️ 工具達標 0 條
人工 5 條(HIGH 1/MEDIUM 2/LOW 2)
🔴 資料庫實查坐實無隔離
報告
§6

🔴 三棒收口(arc 完成,2026-09-11 掃完/2026-09-12 首腦驗收)

三棒合計 14 條發現:B1 五條(HIGH 3/MEDIUM 1/LOW 1)、B2 五條(HIGH 2/MEDIUM 1/LOW 1/記錄 1)、B1b 五條(HIGH 1/MEDIUM 2/LOW 2)。範圍外的舊案重複九條不計入。

最值得修的前三名:

  1. 檔案存取零歸屬檢查(HIGH,四層全空) — B1-1/B1-3/B1-4 + B2 F1 + B1b-1 是同一個病的五個出口。B1b 的資料庫實查把最後一塊拼圖補上:relrowsecurity = f、policy 0 條、17 個欄位**無 tenant_id`。必須套件側與宿主側同一張卡一起修,拆開等於沒修。
  2. 物件儲存帳密明文可取(HIGH) — B2-A,任何登入者打一支 GET 就拿到,拿到後可完全繞過系統直接開倉庫。三處小改可修,與第 1 項不同檔可平行做。
  3. upload_files 加租戶欄位+開啟資料庫隔離(HIGH) — 第④層防線。注意不可只做這條:建表腳本已警告「開了隔離卻沒有 policy 會讓所有下載當場 404」,且系統共享資產會被一起擋掉。需 migration + 資料回填,獨立排。

開卡時的疑點,五條裡三條成立、兩條被推翻:

開卡疑點 結果
1. 上傳時用使用者可控字串拼存檔路徑 ❌ 推翻(B1 從檔名側、B1b 從落地側各證偽一次;但是執行順序意外擋住的,不是刻意防的,重構會長回來)
2. 任何登入者可替任意檔案編號換下載憑證 ✅ 成立(HIGH,B1-1)
3. 查檔案的資料層完全沒有範圍條件 ✅ 成立(HIGH,B1b-1,已用 DEV 實查坐實)
4. 檔名處理很薄 🟡 一半一半(穿越推翻;無副檔名白名單成立,是「上傳網頁檔被當網頁執行」的燃料;無大小/檔數上限成立)
5. 物件儲存憑證加密預設為關 ✅ 成立(LOW,B2-C)——而且追下去發現更前面就漏了(B2-A,HIGH)

另外三棒都印證同一件事:工具不會照卡片清單走——B1b 卡列八個重點,工具只碰到一個半,其餘全是人工追出來的,包含本 arc 證據等級最高的那一條。

§7

修正卡

現況:已處理。 對應 docs/security-report/M02-file-upload.md:高風險七條全數已修,隨 1.21.0 出貨。裁定不修兩條——M02-8(讀不到自己的儲存設定時借用別家客戶的;決策者 10-01:屬舊資料相容問題,新建客戶會自動繼承總部儲存設定)、M02-11(連雲端儲存不加密;內建容器間通訊,CM-2063)。

§8

座標

  • 套件:~/Projects/Jedicogy/module/jedi-python-package/jedi-file-upload/
  • 宿主接線:app/upload_file/、infra/upload_file/、core/upload_file_wiring.py
  • 相關前案:CM-1594(FR-077 R3,遠端 agent 宿主接線,與本 arc 砍掉的那棒重疊)、CM-1633(FR-081 I3,另一支套件的附件功能)、CM-1559(FR-085 C1,無身分時隔離關掉)
  • 首腦手冊:.claude/skills/security-scan-lead/SKILL.md
  • 跨 arc 總表:security-scan-consolidated/
  • 現況:FR-075/handoff/security-scan-STATE.md
§9

文件

以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。

證據與盤點

文件 類型 標題 最後更新
scan-B1-http-boundary / md 盤點證據 FR-086.B1 掃描報告:檔案上傳下載的 HTTP 邊界與契約(jedi-file-upload) 2026-09-11
scan-B1b-storage-backends / md 盤點證據 FR-086.B1b 掃描報告:儲存後端實作與持久層(jedi-file-upload) 2026-09-11
scan-B2-host-wiring / md 盤點證據 FR-086.B2 掃描報告:宿主接線——儲存解析與租戶憑證 2026-09-11
§10

Notion 卡

卡片內容(決策紀錄、驗收條件)以 Notion 為準,本頁只記座標。

關係 卡號 標題 狀態
母案 CM-1651 FR-086 檔案上傳下載(jedi-file-upload)資安掃描(三棒,只掃不修) —
子卡 CM-1652 B1 HTTP 邊界與契約(35 檔) 修正待驗證
子卡 CM-1653 B2 宿主接線:儲存解析與租戶憑證(10 檔,BE repo) 修正待驗證
子卡 CM-1654 B1b 儲存後端實作與持久層(26 檔) 修正待驗證