掃完沒有找到經過投票確認成立的問題。 工具提出 2 條疑慮,三個獨立的檢查員都判定不成立。人工逐條核對時另外記下 3 個值得留意、但不到漏洞程度的小地方(見第 4 節),都不急,可以等下次動到這些檔時順手處理。
客戶的稽核證據檔存放在客戶自己主機上的 Agent,主系統只記一筆「檔案在哪」的紀錄。這一棒查的是:
範圍共 5 個檔、約 1241 行(api/common/file_protect.py、core/plugins/_file_ownership.py、infra/upload_file/remote_agent_adapter.py、core/plugins/_agent_local_runs.py、core/plugins/_bench_longpoll.py)。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 結果 |
|---|---|---|---|---|---|
| C1 | 懷疑附件可以掛上別家公司的檔案編號 | 讀到別家公司的檔 | 有資源庫編輯權限的帳號 | 檔案資料表 public.upload_files |
不成立(3 票全判不成立) |
| C2 | 懷疑管理員填的儲存位址,會讓伺服器去連任意網址 | 伺服器被騙去探內網 | 有儲存設定權限的管理員,而且系統關掉了 Agent 身分驗證 | infra/upload_file/remote_agent_adapter.py:299-305 |
不成立(3 票全判不成立) |
被否決的理由:
scripts/init/02-schema.sql,那是已經過期的出貨基線。實際釘的 jedi-file-upload 套件 migration(004-upload-files-tenant-owner-rls.sql)已經在檔案表加上公司欄位,也開了 RLS(就是「每個客戶只能看自己資料」的資料庫隔離機制)。remote_agent_adapter.py:300-302 在連線之前就直接回「找不到」;出貨安裝程式(scripts/installer/install.sh)每次安裝和升級都會寫入 AGENT_AUTH_MODE=full,也就是一律開著。這幾條都是我自己打開檔案看的,沒有經過三人投票。
remote_agent_adapter.py:96-107 get_file:查檔有沒有公司隔離。 人工查過:成立(有守住)。adapter 本身不檢查「這個人能不能看」,靠兩層擋:資料庫層由套件 migration 開的 RLS 隔離公司;功能層由下載端點先問檔案歸屬登記。_verify_integrity(:192-203)會重算 SHA-256 雜湊值,和上傳當下記的值比對,不一致就寫稽核紀錄並擋下。舊資料沒有記雜湊值的就跳過比對,這是刻意的相容處理。
:71-94 save_file:檔名送給 Agent;Agent 回的 file_uid 只檢查是不是字串。 人工查過:屬實,但風險低。:91 只檢查「是非空字串」,沒有像 _agent_local_runs.py:33 的 _AGENT_FILE_UID 那樣限制字元形狀。這個值之後只會在對同一台 Agent 下指令時帶回去(:105、:129),所以就算 Agent 被攻陷、回了怪字串,影響的也只是它自己那台。 → 小地方 1:建議 :91 也套用 agent_file_uid_ok(),和自主執行報告那條路用同一套檢查。
:109-117 fetch_agent_blob 的 agent_file_uid 由誰給;:118 delete_file 的權限。 人工查過:成立(有守住)。唯一的呼叫端是 _agent_local_runs.py:213,它傳進來的值先經過 _report_file_uid()(:73-75)的正規式形狀檢查。delete_file 本身不檢查權限,由上層的檔案歸屬政策用「write」動作先問過才會呼叫到。這一段在本棒範圍外,沒有逐行追,只確認了 adapter 的設計是這樣。
_file_ownership.py、file_protect.py 新增的歸屬類型:「讀=能力點、刪=一律拒」有沒有套對。 人工查過:成立。刪除都比讀取嚴格:匯入類只給上傳者本人(:100-102);框架 PDF 一律拒(:219-220);資源庫建議附件一律拒(:361);檢測規則包的系統公版和別家公司的不能刪(:272)。每份歸屬答案查詢出錯時都回「不認領」(:83-91),也就是出錯時預設擋下來,不會預設放行。 → 小地方 2:EvidenceBatchFileOwnershipProvider 拒絕時拋的是 ForbiddenError(403,:337),其他各份都回 NotFound(404)。專案規範要求一律回 404,因為回 403 等於告訴對方「這個檔案編號存在」。影響是可以探出某個編號存不存在,不能拿到檔。 → 小地方 3:file_protect.py:51 的 unzip 預設是 True,Agent 送來的報告檔名如果是 .zip,下載時會被拆開重新包裝。_unzip(:106-114)已經先檢查解壓上限,所以不會被壓縮炸彈打爆,但拆開的檔名直接沿用 zip 裡的名字。建議 _agent_local_runs 那條路呼叫 send_protected 時明確傳 unzip=False(檔頭註解 :43-44 本來就是這樣要求的)。
_agent_local_runs.py:Agent 產的 HTML 報告,回應有沒有 CSP sandbox、nosniff、強制下載。 人工查過:成立(有守住)。對應的端點在 api/remote_agent/routes/agent_local_run_route.py:
:57-61):走 send_protected,以 attachment 強制下載,加上 nosniff(要求瀏覽器不要自己猜檔案型別);:67-72):固定用純文字回,加上 nosniff;:83-91):加上 CSP sandbox; default-src 'none',報告裡的腳本、表單、外部連線都不會執行,另外有 nosniff 和 X-Frame-Options: SAMEORIGIN。_bench_longpoll.py:免登入、會佔住執行緒的假端點,只有 DEBUG 或 BENCH_LONGPOLL=1 時才掛上(main.py:197-199)。 人工查過:出貨預設不會掛上,但有一個前提要留意。正式 image 的預設是 DEBUG=false(docker/production/Dockerfile:354);docker/、scripts/installer/、scripts/init/ 裡都沒有設定 BENCH_LONGPOLL。不過同一份 Dockerfile 的註解(:326)寫著 STG 現況是 DEBUG=true,客戶的 env 檔也可以覆寫這個值。一旦打開 DEBUG,這支不用登入的端點就會出現,一次最多佔住一條執行緒 60 秒。任何人連續打它,都能把其他使用者的 API 拖慢。 → 不列為漏洞(要先有人違反「正式環境不開 DEBUG」的設定),但建議掛載條件只認 BENCH_LONGPOLL=1,不要綁 DEBUG。
| 欄位 | 值 |
|---|---|
| run ID | wf_2426c728-42c |
| 掃描的版本 | 058a84c02409190dadc109f1bddb86afd2027abe |
| effort | low,只看正式程式碼(attack-surface) |
| verification.status | verified |
| reason_kind | 無 |
| candidates(交給檢查員的疑慮) | 2 |
| panel_votes | 6(三個檢查員對 2 條各投一票,全數投出) |
| researchers_dispatched / returned | 2 / 2 |
| 成立的發現 | 0 |
| 耗時 | 約 104 分鐘 |
| 歸檔位置 | docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-204012/ |
candidates: 2 表示面板確實有疑慮可投、而且投完了,不是「沒東西可投」的空 verified。
沿革見 LOG 與 git log。