FR-187.I 掃描報告:證據檔經長輪詢存取、自主執行報告、測試端點

1. 一句話結論

掃完沒有找到經過投票確認成立的問題。 工具提出 2 條疑慮,三個獨立的檢查員都判定不成立。人工逐條核對時另外記下 3 個值得留意、但不到漏洞程度的小地方(見第 4 節),都不急,可以等下次動到這些檔時順手處理。

2. 這一棒在檢查什麼

客戶的稽核證據檔存放在客戶自己主機上的 Agent,主系統只記一筆「檔案在哪」的紀錄。這一棒查的是:

  • 下載、上傳、刪除這些證據檔時,有沒有可能拿到別家公司的檔;
  • 下載 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)。

3. 掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 結果
C1 懷疑附件可以掛上別家公司的檔案編號 讀到別家公司的檔 有資源庫編輯權限的帳號 檔案資料表 public.upload_files 不成立(3 票全判不成立)
C2 懷疑管理員填的儲存位址,會讓伺服器去連任意網址 伺服器被騙去探內網 有儲存設定權限的管理員,而且系統關掉了 Agent 身分驗證 infra/upload_file/remote_agent_adapter.py:299-305 不成立(3 票全判不成立)

被否決的理由:

  • C1:研究員引用的是 scripts/init/02-schema.sql,那是已經過期的出貨基線。實際釘的 jedi-file-upload 套件 migration(004-upload-files-tenant-owner-rls.sql)已經在檔案表加上公司欄位,也開了 RLS(就是「每個客戶只能看自己資料」的資料庫隔離機制)。
  • C2:只要 Agent 身分驗證開著,remote_agent_adapter.py:300-302 在連線之前就直接回「找不到」;出貨安裝程式(scripts/installer/install.sh)每次安裝和升級都會寫入 AGENT_AUTH_MODE=full,也就是一律開著。

4. 首腦指定「重點看什麼」逐條人工查證

這幾條都是我自己打開檔案看的,沒有經過三人投票。

  1. remote_agent_adapter.py:96-107 get_file:查檔有沒有公司隔離。 人工查過:成立(有守住)。adapter 本身不檢查「這個人能不能看」,靠兩層擋:資料庫層由套件 migration 開的 RLS 隔離公司;功能層由下載端點先問檔案歸屬登記。_verify_integrity(:192-203)會重算 SHA-256 雜湊值,和上傳當下記的值比對,不一致就寫稽核紀錄並擋下。舊資料沒有記雜湊值的就跳過比對,這是刻意的相容處理。

  2. :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(),和自主執行報告那條路用同一套檢查。

  3. :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 的設計是這樣。

  4. _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 本來就是這樣要求的)。

  5. _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。
  6. _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。

5. 這份結果可信到什麼程度

  • 「這些存在嗎」:C1、C2 都經過三個獨立檢查員投票,6 票全數投出,全部判定不成立,可信度高。第 4 節的三個小地方是我人工開檔看到的,行號都可以直接打開核對,但沒有經過投票。
  • 「只有這些嗎」:不能保證。這次用的是 low 等級:一位研究員讀完整個範圍,另一位專找寫死在程式裡的密碼。兩位都有回報,沒有漏掉沒讀的人;但工具每次跑的結果不完全一樣,沒報不代表一定乾淨。範圍外的呼叫端(下載端點、歸屬政策本體)只有為了核對才去看,沒有完整掃過。

6. 執行概況(照 stamp 抄)

欄位 值
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。