app/upload_file/+infra/upload_file/+core/upload_file_wiring.py+di_containers/upload_file/,共 10 檔(BE 主專案)8aa36333(BE repo,branch feature/FR-075,工作區有未提交異動)low effort,focus attack-surfaceverifiedB1 問「套件那半邊有沒有檢查這個檔是不是你的」,答案是沒有;B2 問「宿主這半邊有沒有補上」,答案也是沒有——四層防線(網址入口、業務邏輯、資料查詢、資料庫本身)從頭到尾一個檢查都沒有,中間沒有任何一層會擋。
宿主給套件的「守門」只有一行 jwt_required(),意思是**「只要能登入就放行」**;再往下的業務邏輯只做一件事——拿檔案編號去問「這個檔存在哪個後端」,然後把檔案吐出來。
這一棒的真正產出只有一條發現(F1)。 工具另外報的九條全是同一批已知憑證外洩問題被第 N 次撈到,不是新發現(詳見第 3 節下半與第 4 節)。
另外人工查到兩件工具沒報的事,都與卡片重點直接相關:
jedi-file-upload 這支套件只懂「檔案上傳下載長什麼樣子」,它不知道 Guidant AI 的檔案實際上存在哪裡。「存哪裡」是產品自己的事,所以主專案寫了這 10 個檔當作接線盒,回答套件三個問題:
租戶=一家客戶。這套產品一份程式同時服務多家客戶,靠「租戶」這個標記把資料分開。 物件儲存=專門放檔案的外部服務(像公司自架的網路硬碟),連它要帳號密碼。
B1 已經確認套件那一側沒有做歸屬檢查。這一棒要回答的核心問題就是一句:「宿主這一側有沒有補上?」
只找問題、不修問題——底下所有修法都只是建議,這一棒沒有動任何一行程式碼,沒有真的上傳或下載任何檔案,也沒有拿任何帳密去連線試。全部是讀程式碼、讀設定與唯讀查資料庫 schema 推論出來的。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 嚴重度 | 來源 |
|---|---|---|---|---|---|---|
| F1 | **拿檔案編號要檔案時,完全不檢查「這個檔是不是你的」——程式只拿編號去查「這檔存在哪個後端」,然後直接讀/轉檔/刪除 | 任何登入者可以下載、線上預覽、永久刪除別家客戶的檔案**(稽核證據、SSP 文件、問卷附件) | 任何有效帳號 + 知道一個檔案編號 | app/upload_file/service/managed_file_upload_service.py:352-362(三支方法)+ :173-178(挑後端的那一段) |
HIGH | 工具 3/3 票 + 首腦開檔核對 |
| B2-A | 物件儲存的帳號密碼會明文回給任何登入者——遮罩清單漏掉儲存設定這一組,而且那支查詢端點沒有權限檢查,只要登入就能打 | 拿到帳密後可以繞過整個系統直接連物件儲存,把該租戶所有檔案列出來、下載、覆蓋或刪掉 | 任何有效帳號(不需任何角色) | 遮罩清單 api/system_config/routes/system_config_route.py:35-39;沒掛權限檢查的讀取端點 :202-207、:367-372;憑證讀進程式的地方 app/upload_file/service/managed_file_upload_service.py:99-119 |
HIGH | 工具未報,人工查證 |
| B2-B | A 客戶讀不到自己的儲存設定時,程式會去借 B 客戶的帳號密碼來連線 | 檔案可能被存到/讀自別家客戶的儲存空間,租戶界線在儲存這一層不成立;程式自己只記一行警告,不擋 | 檔案的儲存設定在本租戶查不到(歷史錯位檔、換過後端) | app/upload_file/service/managed_file_upload_service.py:218 呼叫 :263-277,實際繞隔離的查詢在 infra/upload_file/system_storage_config_reader.py:51-58 |
MEDIUM | 工具未報,人工查證 |
| B2-C | 連物件儲存預設不加密(secure 預設關),帳密與檔案內容在內網以明文傳輸 |
能看到內網流量的人可以側錄到儲存帳密與檔案內容 | 能監看內網流量(同網段、被入侵的網路設備) | app/upload_file/service/managed_file_upload_service.py:105/:119;套件端預設值 jedi-file-upload/.../ports/dto/minio_upload_config_dto.py:11 |
LOW | 工具未報,人工查證 |
| B2-D | 客戶端 agent 通道的認證預設整個關閉(AGENT_AUTH_MODE 預設 none) |
預設狀態下雲端與客戶設備之間不驗憑證、不簽章、走明文 http | 能接觸該網路路徑者 | common/util/agent_auth_settings.py:22(預設 none)、config/config.py:261;受影響的轉發在 infra/upload_file/remote_agent_adapter.py:200-207 |
記錄用(歸 FR-077 CM-1594) | 工具未報,人工查證 |
工具的密鑰專項掃描(focus 開啟時會附帶跑的一次全 repo 憑證搜查)不受範圍限制,所以它每一棒都會把同一批問題再撈一次。這九條沒有一條是新發現:
| 工具編號 | 一句話 | 已經記在哪 |
|---|---|---|
| F2 | 三家 AI 服務的 API 金鑰被寫進對話紀錄檔 | CM-1607(已撤銷,清版控殘留)+ FR-079 B2 F1/F2/F3 |
| F3 | POC 資料庫密碼硬編在一支腳本裡 | CM-1608(決策者已裁:測試機專用、密碼不需更換,只做程式碼衛生) |
| F4 | 資料庫管理員(可繞過租戶隔離)的密碼寫進一份分析文件 | CM-1629(同一組密碼散落 249 個檔) |
| F5 | 用來加密 Google 雲端硬碟權杖的金鑰被寫進版控 | CM-1631(Google 密鑰與加密金鑰外洩,22 檔) |
| F6 | GitLab 個人存取權杖寫進對話紀錄與一份設定傾印 | CM-1607 + FR-079 B2 F1/F2/F3 |
| F7 | 登入用的簽章金鑰寫進對話紀錄 | CM-1607(開發機那把;各安裝版自己產生) |
| F8 | 共用測試帳號的密碼寫在參考文件裡 | CM-1608(同上裁決) |
| F9 | 出貨用的資料庫初始化腳本,每一套安裝都內建同一組原廠最高權限密碼 | 跨 arc 總表 §3.1 第 3 項(FR-079 B2 F15,決策者已裁「先記錄,之後看怎麼調整」) |
| F10 | 另一份設定傾印裡的 LangChain 權杖與物件儲存帳密 | CM-1607(清版控殘留)+ CM-1631 同批 |
這九條唯一的新資訊價值是:密鑰專項每掃一棒就把同一批撈回來一次,佐證那批憑證確實還沒清乾淨。 除此之外請把它們當成已知項,不要重複開卡、不要計入本棒成果。
⚠️ 本報告一律不寫任何金鑰值或密碼值,只寫「哪個檔、第幾行、對應哪張卡」。要看實際值請直接開那些檔案或查對應卡片。
這是什麼問題(白話)
主專案寫了一支服務叫 ManagedFileUploadService,它負責「讀檔、轉 PDF、刪檔」這三件事。三支方法長得一模一樣,都只有一行:
@transaction
def get_upload_file(self, uid: str):
return self._adapter_for_uid(uid).get_file(uid) # :352-354
@transaction
def convert_to_pdf(self, file_uid, output_folder, output_filename=None):
return self._adapter_for_uid(file_uid).convert_to_pdf(...) # :356-358
@transaction
def delete_file(self, uid: str) -> bool:
return self._adapter_for_uid(uid).delete_file(uid) # :360-362_adapter_for_uid 的意思是「依這個檔案編號,決定要用哪個儲存後端去拿它」。它自己的說明文字(docstring,就是程式裡給人看的註解)寫得很清楚:
依「檔案自己的 storage_scope」選 adapter
關鍵字是「檔案自己的」——它看的是檔案那一列資料自己記著什麼,從頭到尾不看呼叫的人是誰。整個函式(:173-178)只有四行,沒有任何一行在問「這個人有沒有資格碰這個檔」。
再往下一層也沒有兜底:套件的資料查詢(UploadFileRepoImpl.get_by_uid)只用編號一個條件去撈,沒有任何範圍限制;而存檔案的那張資料表 upload_files 連「這是哪家客戶的」這個欄位都沒有(見下方「首腦核對註記」),所以資料庫層也不可能幫忙擋。
出事會怎樣
delete_file 會把儲存裡的檔案本體與資料庫紀錄一起刪除,物件儲存那條還會連帶清掉轉檔產生的 PDF 快取。刪了救不回來,而引用這個檔案的資料(案件、證據、答案)會變成指向一個不存在的檔。要先有什麼才打得到
在哪裡
app/upload_file/service/managed_file_upload_service.py:352-354 get_upload_file(讀檔)
app/upload_file/service/managed_file_upload_service.py:356-358 convert_to_pdf(轉 PDF 預覽)
app/upload_file/service/managed_file_upload_service.py:360-362 delete_file(刪檔)
app/upload_file/service/managed_file_upload_service.py:173-178 _adapter_for_uid(只看檔案自己記的 scope,不看人)
core/upload_file_wiring.py:133 auth_required = jwt_required()(只驗「有沒有登入」)
怎麼修
修在 ManagedFileUploadService 這一層是對的位置——本專案的規範明訂「資源類守門放 app service 層」(要先把資源撈出來才知道要判誰,route 層做不到)。
具體做法:在 get_upload_file / convert_to_pdf / delete_file 三支的第一行(_adapter_for_uid 之前)插一道檢查:
common/authz):
assert_project_participant(...)SspPermissionChecker.require_write_access()(刪除走寫入權限)upload_files.created_user 與呼叫者,不符就擋。storage_scope=system 的產品共享資產(合規框架 PDF)本來就該全租戶讀得到,可以明文放行讀取,但刪除仍要擋(改成只有平台管理員能刪)。治本的第二道:給 upload_files 加租戶欄位並開啟資料庫層隔離,讓資料庫成為最後一道防線。這件事要配 migration 與既有資料回填,屬另一張卡的工作量,但沒有它,應用層那一道就是唯一防線。
⚠️ 修的時候必須跟 B1 一起修:只修 B2 這半邊,B1 的「換發免登入通行證」那條還在——對方拿到證之後走的是同一條路,而那條路上的檢查是本條要補的這一道。兩邊是同一件事的兩半。
首腦核對註記(首腦親自開檔,逐項屬實)
:352-362 三個方法(get_upload_file / convert_to_pdf / delete_file)全部只有 self._adapter_for_uid(uid).xxx(uid) 一行,中間零檢查。✅ 屬實:173-177 的 _adapter_for_uid 只看「檔案自己記的 storage_scope」挑 adapter(它的 docstring 自己就這樣寫),完全不看呼叫者是誰。✅ 屬實UploadFileRepoImpl.get_by_uid 也無任何範圍條件;upload_files 表無租戶欄位(建表腳本 scripts/init/02-schema.sql:16839-16857 的欄位清單裡確實沒有 tenant_id)、資料庫層隔離是關的。✅ 屬實(工具未報,人工查證)
這條是卡片第 ③ 點追下去的結果。卡片原本只問「憑證存在資料庫的 JSONB 欄位裡、加密開關預設是關的」,但追下去發現更前面還有一道:那些帳密根本就會被 API 明文回出去。
這是什麼問題(白話)
每個客戶的物件儲存帳號密碼,存在系統設定表裡一個叫 STORAGE_CONFIG 的群組。系統設定有一支通用的查詢端點可以把某一組設定整個讀出來。
這支端點有一道遮罩機制(回傳前把敏感欄位刪掉),問題是它兩邊都對不上:
api/system_config/routes/system_config_route.py:36:
HIDDEN_SECRET = ['SMTP', 'THIRD_PARTY_LOGIN', 'NOTIFY_CONFIG', 'ISSUE_INTEGRATE_CONFIG']STORAGE_CONFIG 不在裡面,所以儲存設定這一組完全不進遮罩流程。:39):
_SECRET_KEYS = ('secret', 'private_token')secret_key / minio_secret_key / access_key(見 managed_file_upload_service.py:101-119)——一個都不在名單裡。更關鍵的是:那支讀取端點沒有任何權限檢查。 同一個檔案裡,寫入類動作(改設定、刪設定)每一支都呼叫了 _assert_config_capability(...) 做能力檢查,唯獨讀取類一支都沒有:
| 端點 | 有沒有能力檢查 |
|---|---|
GET /api/1.0/system/configs/<group>(:202-207) |
❌ 只有 @jwt_required() |
GET /api/1.0/system/config/<group>/<key>(:367-372) |
❌ 只有 @jwt_required() |
PUT(改)/DELETE(刪)/POST(新增) |
✅ 有(:271/:299/:326/:397/:420) |
出事會怎樣
任何登入者打一支 GET,就拿到自己租戶物件儲存的位址、帳號、密碼明文。拿到之後就不必再經過這個系統了——可以用標準的 S3 工具直接連上去,把該租戶所有檔案列出來、整批下載、覆蓋、刪除。應用層再怎麼補歸屬檢查都管不到,因為對方根本不從應用層進來。
這條與 F1 是兩件不同的事:F1 是「繞過檢查拿單一檔案」,這條是「拿到鑰匙直接開倉庫」。
要先有什麼才打得到
在哪裡
api/system_config/routes/system_config_route.py:36 HIDDEN_SECRET 清單漏了 STORAGE_CONFIG
api/system_config/routes/system_config_route.py:39 _SECRET_KEYS 只有 secret / private_token
api/system_config/routes/system_config_route.py:202-207 GET by group,只掛 @jwt_required()
api/system_config/routes/system_config_route.py:367-372 GET by group+key,只掛 @jwt_required()
app/upload_file/service/managed_file_upload_service.py:99-119 憑證從 JSONB 讀進來的地方(本棒範圍內)
範圍註記:
api/system_config/這個檔不在本棒的 10 檔範圍內(本棒範圍只到憑證被讀進程式的那一行)。之所以追過去,是因為卡片第 ③ 點問的就是「憑證怎麼保管」,不追到讀取端就答不完整。建議獨立開卡,與本棒的 F1 分開。
怎麼修(三件都要做,缺一還是漏)
STORAGE_CONFIG 加進 HIDDEN_SECRET(:36)。_SECRET_KEYS 補上 secret_key、minio_secret_key、access_key、minio_access_key(:39)。_assert_config_capability(group, "read")(寫入類已經這樣做了,:271 是現成範例,不要另外發明第二套守門)。做完要寫測試。 本專案的測試政策雖然對小改動預設不寫測試,但授權守門屬於明文列出的「核心共用邏輯」例外——請針對「非管理員打 GET 拿不到 secret_key 欄位」寫一支測試釘住。
(工具未報,人工查證;同時回答卡片第 ② 與第 ④ 點)
這是什麼問題(白話)
要讀一個舊檔案時,程式得先知道「用哪組帳密連哪個儲存」。正常路徑是查本租戶的儲存設定。但如果查不到(例如這個檔案當初被存錯後端、或後端換過),程式不會停下來報錯,而是跨過租戶界線去找別家客戶的同型設定來用:
config = self._load_config_for_storage_type_any_tenant(file_type) # :218
if config is not None:
logger.warning("...已跨租戶 fallback 借同型設定讀取...") # :220-224_load_config_for_storage_type_any_tenant(:263-277)會呼叫 read_all_storage_config_values(),那一支(infra/upload_file/system_storage_config_reader.py:51-58)最終跑的是一句明確關掉租戶隔離的資料庫查詢(SET LOCAL app.is_super_admin = 't',見 infra/system_config/system_config_root_reader.py:110-133),把所有租戶的儲存設定全部撈回來,然後取第一筆型別相符的。
程式碼註解對此有自覺,說明實際檔案位置會用「檔案自己記的路徑」覆寫,所以不會讀錯物件。問題不在讀錯檔,在於「A 客戶的操作,用了 B 客戶的帳號密碼去連線」——而且它只記一行警告就繼續跑。
為什麼是 MEDIUM 不是 HIGH:要打到它需要滿足一個不是攻擊者能直接控制的前提(該檔的儲存型別在本租戶查無設定)。它比較像是租戶界線在程式裡被刻意開了一個口,而不是一條可以主動觸發的攻擊路徑。但這個口一旦被別的 bug 碰到,就是跨租戶的。
在哪裡
app/upload_file/service/managed_file_upload_service.py:211-230 讀不到本租戶設定 → 走跨租戶 fallback
app/upload_file/service/managed_file_upload_service.py:263-277 跨租戶查詢的實作
infra/upload_file/system_storage_config_reader.py:51-58 read_all_storage_config_values(全租戶)
infra/system_config/system_config_root_reader.py:110-133 真正關掉隔離的那一句 SQL
怎麼修
短期:把這條 fallback 的觸發範圍收窄——只在「檔案本身沒有租戶歸屬」(系統共享資產)時才允許跨租戶借設定,其餘情況直接拋既有的 FILE_STORAGE_CONFIG_TYPE_NOT_FOUND 讓錯誤浮出來。並把 logger.warning 升級成稽核事件(audit(...)),讓這件事有紀錄可查而不是只留一行 log。
長期:程式註解自陳這條 fallback 是為了處理「歷史錯位檔」(背景 job 曾把檔存進別租戶的後端)。正解是把那些錯位檔搬回正確位置,然後把這條退路整個拿掉——它現在是拿「永久放寬租戶界線」在換「幾筆歷史資料能讀」。
(工具未報,人工查證;回答卡片第 ③ 點的後半)
儲存設定裡有一個開關叫 secure,決定連物件儲存時要不要走加密連線(就是網址上 https 與 http 的差別)。這個開關兩邊的預設值都是「關」:
secure=v.get('secure', False), # managed_file_upload_service.py:105(MinIO)
secure=v.get('secure', False), # managed_file_upload_service.py:119(SeaweedFS)secure: bool = False # 套件端 ports/dto/minio_upload_config_dto.py:11這個值被原封不動交給連線用的程式庫(jedi-file-upload/.../minio_adapter.py:59)。secure=False 的意思是整條連線都不加密:帳號密碼與檔案內容都以明文在網路上傳輸。
出事會怎樣:能看到那段網路流量的人(同網段、被入侵的交換器或路由器)可以側錄到儲存帳密,之後就等同 B2-A 的後果。
為什麼只評 LOW:物件儲存通常與應用服務在同一台機器或同一個內網(落地版是同一個 docker stack),攻擊者要先進到那個網路才打得到——他到了那個位置通常已經有更直接的手段。但**「預設關閉」這件事本身是設計缺陷**——安全選項的預設值應該是安全的那一邊。
怎麼修:把兩處預設值改成 True,並在部署文件明寫「內網自簽憑證的環境要顯式設 secure: false」。改預設值會影響既有部署(現有設定沒填 secure 的會突然改走加密而連不上),所以這條必須配合升級說明一起上,不能默默改。
(工具未報,人工查證;回答卡片第 ⑦ 點)
卡片明訂 remote_agent_adapter.py(273 行,本棒 10 檔中最大的一支)只讀不深追,完整掃描屬 FR-077 的 CM-1594。照辦,這裡只記讀到的一件事:
這支 adapter 負責把檔案操作轉發到客戶自己機房裡的 agent 設備。它有一套完整的保護(雙向憑證、短效簽章、回應指紋比對、撤銷檢查),寫得相當認真——但整套的開關預設是關的:
mode=(os.getenv("AGENT_AUTH_MODE", "none") or "none").strip().lower() # common/util/agent_auth_settings.py:22AGENT_AUTH_MODE = os.getenv("AGENT_AUTH_MODE", "none") # config/config.py:261none 那條分支(remote_agent_adapter.py:200-207)直接用設定裡的網址發請求,不加任何憑證、不強制 https。程式註解自陳理由是「維持現狀,demo 不破」。
不在本棒下判斷——這條的完整評估(含客戶端那一側)請看 CM-1594。這裡只是把預設值這件事記下來,免得下一棒以為「有寫 mTLS 就等於有保護」。
唯一附帶確認的好消息:檔案完整性檢查(_verify_integrity,:113-128)是無條件執行的,不受這個開關影響——下載時會重算檔案指紋比對,不符就擋下並記稽核。這一段做得對。
這是本報告最重要的一節。 B1 與 B2 各自只看到半條路,合起來才看得到全貌。
B1(CM-1652,已驗收)證實:套件那一側四個對外端點零歸屬檢查。 B2(本棒)回答了 B1 留下的問題——「宿主那半邊有沒有補上?」答案是:沒有。
一個請求從外面打進來,理論上會經過四道關卡。實際上四道全是空的:
| 第幾層 | 這一層應該做什麼 | 實際上做了什麼 | 誰查到的 |
|---|---|---|---|
| ① 網址入口(route) | 確認「你是誰」+「你能不能碰這個檔」 | 只確認「你有沒有登入」。宿主交給套件的守門就一行 jwt_required()(core/upload_file_wiring.py:133),連 ?st= 通行證那條的發證窗口也只驗登入 |
B1 + B2 |
| ② 業務邏輯(app service) | 把檔案的所屬資源撈出來,判定呼叫者有沒有權限 | 什麼都不做。三支方法各一行,只拿編號去挑後端(managed_file_upload_service.py:352-362),挑後端的函式明文只看「檔案自己記的 scope」、不看人(:173-178) |
B2 F1(本棒) |
| ③ 資料查詢(repository) | 查詢時至少帶一個範圍條件(你的租戶/你的專案/你建的) | 只用檔案編號一個條件,沒有任何範圍限制(UploadFileRepoImpl.get_by_uid) |
B1 追脈絡 + B2 核對 |
| ④ 資料庫本身(table/RLS) | 靠資料庫的租戶隔離當最後一道防線 | 那張表連「這是哪家客戶的」欄位都沒有(scripts/init/02-schema.sql:16839 起的欄位清單),隔離機制也是關的——想擋也擋不了 |
B2 核對 |
RLS(就是「每個客戶只能看自己資料」的資料庫隔離機制)——資料庫層級的自動過濾。這張表上沒開,而且就算開了也沒用,因為表上沒有可以用來判斷歸屬的欄位。
結果:從網址進來到檔案吐出去,沒有任何一個地方會問「這個檔是不是你的」。
而且還有一個加乘:B1 發現的「免登入下載通行證」那條路,走的時候程式會把租戶標成「無」,而系統把「無租戶」當成最高權限、跳過所有隔離(jedi-iam/.../signed_token.py:88-107 的 _set_minimal_context,docstring 自陳 tenant_id=None → 判為 super)。所以那條路不只沒有歸屬檢查,它連原本就很薄的資料庫保護都一併關掉。(這與 CM-1559/FR-085 C1「無身分時隔離關掉」是同一個模式。)
upload_files 加租戶欄位+開啟資料庫隔離需要 migration 與既有資料回填,工作量與風險都跟前兩者不同。但沒有它,應用層那一道就是唯一防線——而這份報告已經證明,唯一防線是會被漏掉的。| 卡片項目 | 結論 | 依據 |
|---|---|---|
| ① 挑儲存後端時有沒有驗歸屬 | **❌ 沒有,成立(HIGH) | 見 F1**。工具 3/3 票 + 首腦開檔核對。_adapter_for_uid(:173-178)的 docstring 自陳只看「檔案自己的 storage_scope」;三支對外方法(:352-362)各一行、中間零檢查 |
| ② 跨租戶借用憑證的 fallback | **✅ 確實存在(MEDIUM) | 見 B2-B**。卡片標的 :46 是 docstring 講 remote_agent 的段落;真正的跨租戶借用在 :211-230(呼叫 :263-277 → system_storage_config_reader.py:51-58 → 一句關掉隔離的 SQL)。程式只記一行 warning 就繼續,不擋 |
③ 物件儲存憑證存 JSONB 且 secure 預設 False |
✅ 成立,而且比卡片以為的更嚴重 | 見 B2-A(HIGH) 與 B2-C(LOW)。卡片問的是「存得安不安全」,追下去發現更前面就漏了:遮罩清單沒含 STORAGE_CONFIG(:36)、欄位名單沒含 secret_key(:39)、而且兩支 GET 端點根本沒有能力檢查(:202-207/:367-372)→ 任何登入者可讀明文帳密。secure 預設關(:105/:119)另記 LOW |
④ 三支繞過隔離的超級管理員讀取(system_storage_config_reader.py:21-57) |
分兩半:兩支合理、一支就是 ② 的成因 | 逐支看過:read_root_storage_config_value(:21-33)與 read_tenant_storage_config_value(:36-48)都釘死在一個指定的租戶+指定的 group/key,範圍極窄、有明確業務理由(系統共享資產/背景 job 無租戶脈絡),判定合理。但 read_all_storage_config_values(:51-58)把所有租戶的儲存設定全撈,它就是 B2-B 的執行機關——問題不在這支 reader 本身,在於 managed_file_upload_service.py:218 拿它來當「讀不到就借別人的」退路 |
⑤ 吞例外(core/upload_file_wiring.py:104) |
✅ 確實是「什麼錯都吞」,但沒有資安影響 | except Exception: logger.warning(...); return None(:104-106)。它包的是「查使用者暱稱」這件事,吞掉之後的後果是畫面顯示帳號而不是暱稱,不影響任何權限判斷、不會放行任何請求。這是出錯就降級顯示,不是出錯就放行。 程式碼註解自己也把這個降級的副作用寫得很清楚(暱稱恆為 null 很難查)。建議但不急:把 except Exception 縮小到預期的例外型別,免得日後把真正的程式錯誤也一起吞掉 |
⑥ di_containers/upload_file/ 接線有沒有漏傳守門 |
**沒有漏,但「有傳」不等於「有用」 | upload_file_containers.py(34 行)只組裝服務、完全不涉及守門**,看不出漏不漏。守門實際是在 core/upload_file_wiring.py:124-139 交給套件的,三件都有傳(auth_required / file_access_guard / signed_token_issuer),所以套件開機時的檢查(B1-8 查證的 _assert_api_wiring,缺件就拒絕啟動)會過。真正的問題是傳進去的東西太弱——auth_required=lambda fn: jwt_required()(fn)(:133)只驗身分。套件的防呆只檢查「有沒有傳」,不檢查「傳的東西夠不夠」,於是「宿主接了一個空守門」這件事在開機時完全看不出來。順帶確認一件做得對的事:服務是以 provider(工廠)形式傳入而非現成實例(:128-129,檔頭有專段說明),避免了併發下共用同一個物件的問題 |
⑦ remote_agent_adapter.py 只讀不深追 |
照辦。記一件事,判斷歸 CM-1594 | 見 B2-D。該檔有完整的雙向憑證+簽章+指紋保護,但總開關 AGENT_AUTH_MODE 預設是 none(common/util/agent_auth_settings.py:22、config/config.py:261),關閉時走明文 http、不帶任何憑證(:200-207)。完整評估請派 FR-077 的 CM-1594,不要在本 arc 重開。 附帶好消息:檔案完整性比對(:113-128)無條件執行、不受開關影響,這段做得對 |
卡片沒問但順手確認的:本棒 10 檔的程式碼裡沒有任何硬編的帳密或金鑰(人工逐檔看過)——憑證全部從資料庫設定或環境變數讀取,這一點是對的。工具報的那九條憑證問題全都不在這 10 檔裡(見第 3.2 節)。
分兩層講,兩層的答案不一樣。
verified,36 票全數投出(12 條候選 × 3 位檢查員),沒有一票漏投、沒有一條候選未審。五件事要說清楚:
low 檔次的掃描——一位研究員讀完全部範圍,不做威脅建模、不跑廣度掃描。深度夠,但不是地毯式。合起來的建議:F1 這條可以直接開修正卡(證據是事實陳述、首腦核對過)。但不要因為這棒掃完了,就宣稱這 10 個檔已經乾淨——真正被三人面板審視過的只有 1 條,其餘都是人工補的。若日後這塊要再掃一次,把密鑰專項關掉(focus 不開或範圍內先清乾淨),讓研究員的注意力全部落在範圍內。
| 項目 | 值 |
|---|---|
| 掃描範圍 | app/upload_file/+infra/upload_file/+core/upload_file_wiring.py+di_containers/upload_file/,10 檔(與卡片數字一致;實際 910 行,其中 4 個是空的 __init__.py) |
| scanRoot | BE 主專案 compliance-manager-be/(與 B1/B1b 的 jedi monorepo 不同) |
| commit | 8aa36333777342cbf4cfc1eb45e58d529081f64e,branch feature/FR-075,工作區有未提交異動(dirty: true) |
| effort/focus | low/attack-surface |
| 研究員 | 派出 2、回來 2(第一位中途停滯五次後完成,故耗時長,但無研究產出遺失) |
| 候選發現 | 12 條 → 去重後 12 條(無重複可去) |
| 面板票數 | 36 票(12 條 × 3 位檢查員),全數投出、0 票未投、0 條候選未審 |
| 達標發現 | 10 條(另 2 條被三位檢查員一致否決) |
| 嚴重度分布(工具報的) | CRITICAL 0/HIGH 6/MEDIUM 4/LOW 0 |
| 其中在本棒範圍內的 | 1 條(F1,HIGH,3/3 票)——其餘 9 條全是範圍外的既有已知憑證問題,見第 3.2 節 |
| 人工另外查到 | 4 條(B2-A HIGH/B2-B MEDIUM/B2-C LOW/B2-D 記錄用),工具全未報 |
| 未達標被否決 | 2 條(工具未列明細) |
| 驗證輪數 | 1 輪(無續跑、無遺失候選、無因上限被砍掉的候選) |
| stamp | verified,reason: null |
| 工具產出 | compliance-manager-be/CLAUDE-SECURITY-20260911-065519/(該目錄自帶 .gitignore,不入版控) |
工具原始報告(英文,含完整攻擊路徑描述):CLAUDE-SECURITY-RESULTS.md / .jsonl / .sarif,同上目錄。
| 建議卡 | 內容 | 嚴重度 | 備註 |
|---|---|---|---|
| **甲:檔案存取歸屬檢查(B1+B2 合併一張) | B1-1/B1-3/B1-4 + 本棒 F1,四個出口同一個病、同一道修法**,套件側與宿主側必須一起改 | HIGH | 拆開修=等於沒修(理由見第 5 節) |
| 乙:儲存憑證明文外洩 | B2-A 三件事(補遮罩群組、補遮罩欄位、兩支 GET 端點補能力檢查) | HIGH | 改的是 api/system_config/,與甲不同檔、可平行做。要寫測試 |
丙:upload_files 加租戶欄位+開啟資料庫隔離 |
第④層防線 | HIGH | 需 migration + 既有資料回填,工作量與風險與甲乙不同,建議獨立排 |
| 丁:跨租戶借憑證的退路收窄 | B2-B | MEDIUM | 動同一個檔(managed_file_upload_service.py),與甲有派工衝突,要序列做 |
戊:secure 預設改為加密 |
B2-C | LOW | 改預設值會影響既有部署,必須配升級說明,不能默默改 |
| (不另開) | B2-D agent 通道認證預設關閉 | — | 歸 FR-077 CM-1594,不在本 arc 重開 |
| (不另開) | 工具報的 F2~F10 九條憑證問題 | — | 全是舊案:CM-1607/CM-1608/CM-1629/CM-1631/總表 §3.1 第 3 項 |
app/upload_file/、infra/upload_file/、core/upload_file_wiring.py、di_containers/upload_file/(BE repo)scan-B1-http-boundary.md(CM-1652,套件的 HTTP 邊界 35 檔)security-scan-consolidated/.claude/skills/security-scan-lead/SKILL.md