本卡屬 FR-086(母卡 CM-XXXX,建完補號),第 3 棒:套件的儲存後端實作與持久層(jedi monorepo)。只掃不修。 建議在 B2 驗收完之後跑。
掃 jedi-file-upload 套件的 infra/+domain/+migrations/ 共 26 檔。這是檔案真正被讀寫的地方:三個儲存後端(主機硬碟、MinIO、SeaweedFS)各自怎麼存怎麼取、檔案編號怎麼查、以及那張存檔案紀錄的資料表長什麼樣。要找的是:路徑怎麼組出來的、查檔案時有沒有範圍限制、有沒有把外部輸入餵給系統指令。只找問題、不修問題。
B1 看的是「請求怎麼進來」,這一棒看的是「檔案怎麼落地」——路徑穿越如果成立,真正寫出界的動作發生在這裡;而「查檔案沒有範圍限制」這條也在這一棒的持久層。兩棒分開是因為心智模型不同(一個看對外契約,一個看實際 I/O),但結論要合看。
首腦與偵察 agent 讀過程式碼後的起點,未經驗證。工具不會照這份清單走,工具沒答的你要自己開檔查,並標明「(工具未報,人工查證)」。
infra/repository/upload_file_repo_impl.py:41-45:get_by_uid 只做 filter_by(uid=_uid),沒有任何租戶或擁有者條件;而這張表沒有多租戶欄位(infra/models/upload_file.py 無 tenant 欄位,migration 檔頭自陳)。要追:(a) 有沒有別的地方補上這道檢查(上層 service?宿主?);(b) 資料庫層對這張表有沒有設隔離規則——請用 DEV 庫實查(SELECT relrowsecurity FROM pg_class WHERE relname='upload_files' 與 SELECT count(*) FROM pg_policies WHERE tablename='upload_files',唯讀);(c) 若都沒有,猜到或列舉到編號就能取得別人的檔案。infra/adapter/local/local_file_adapter.py:38:沒給設定時預設 /tmp/upload/。要追:(a) 實際部署有沒有給設定?(b) 存檔時路徑怎麼拼(B1 的①查的是「使用者可控的字串進來」,這裡查「進來之後有沒有被正規化」);(c) 取檔時 os.path.join 的兩段各來自哪裡;(d) 有沒有檢查「組出來的路徑還在允許的目錄底下」(常見做法是 os.path.realpath 後比對前綴)。infra/adapter/local/local_file_adapter.py:135 與 infra/adapter/minio/minio_adapter.py:263 都用 subprocess.run 叫 LibreOffice 做轉檔。要追:(a) 餵進去的檔名來自哪裡(DB?使用者?);(b) 是用陣列形式呼叫(安全)還是字串加 shell=True(危險);(c) 檔名含特殊字元會怎樣;(d) 轉檔產生的暫存檔放哪、會不會被別人讀到。infra/adapter/minio/minio_adapter.py:58-64:建立連線失敗只 log 一行,client 變成 None,後續呼叫會炸 NoneType。要追:(a) 這個 fail 之後系統處於什麼狀態(每次上傳都失敗?還是靜默落到別的後端?);(b) 錯誤訊息裡有沒有印出 config(config 含憑證,那就是憑證進 log);(c) SeaweedFS 那支有沒有同款寫法。local/minio/seaweedfs 三支 adapter 做同一件事的三種實作。要追:某一支有的安全檢查,另外兩支有沒有漏掉?(同款功能多份實作、只修了一份是本專案反覆出現的形狀——FR-085 C1 剛撞到 Redis 那個漏網複製品。)infra/factory/upload_file_factory.py —— 決定用哪個 adapter 的工廠。要追:找不到對應後端時的 fallback 是什麼?(fail-open 就是大洞——對照 FR-075 S7 查 auth_factory 時的做法:那支是拋 405,正確。)migrations/ 兩支 SQL —— 建表腳本。要追:有沒有 GRANT 給對的角色、有沒有該有而沒有的索引或約束、以及檔頭自陳「本表沒有多租戶欄位」這個設計決定在今天還站得住嗎(它說租戶隔離由「檔案的持有者資源」那層負責,而 B1/B2 要查的正是那層到底有沒有做)。domain/ 那 7 檔 —— 實體與領域服務。要追:有沒有業務規則層級的檢查被跳過,或是 entity 上帶了不該往外送的欄位。.env/憑證:本棒範圍內若撈到憑證,照實記並標明;只做存在性描述,不印值。