檢查日期 2026-09-16|對應卡片 CM-1601|檢查範圍 13 個檔案
四條發現通過檢查、全部中等。 其中三條指向同一個根因:客戶機房的代理程式來雲端拿檔案、或回報任務結果時,雲端完全不驗證對方是誰——身分就是請求自己在標頭上寫的那一行字。任何能連到 API 的人,只要知道一組代理程式編號與一個檔案編號,就能把客戶的源碼壓縮包下載走;只要知道一個任務編號,就能把別人的掃描標成「成功」並塞進假的檢測結果。第四條是越界發現:另一支套件 jedi-issue 的歷史檔裡有 GitLab 與 GitHub 的存取權杖,至今仍能從任何一份 clone 取出。
與先前各棒的關係:三條身分問題與 R1 是同一個根因(控制面端點零認證),但本棒查的是不同的端點、不同的資料,不是重複;F4 的兩個端點(ack/result)R1 已報過(R1 的 F2+F5,修正卡 CM-1595),這次是從任務狀態機那一側再看到同一條路,標「重複 R1 F2/F5」。GitLab 權杖那條與 R1b 的 F2 是同一份歷史檔的不同兩把鑰匙(R1b 那把是寫死在程式碼裡的,這把在 .env 裡),需要一起處理。
卡片重點①「同租戶內冒充」的答案:成立,而且比卡片預想的更寬——跨租戶也成立。2026-09-14 那次修改(fc1cadb)確實讓執行身分被關進正確的租戶,但它是「先用你自己報的編號去查你是哪一家,再切成那一家」——查出來的租戶是攻擊者選的,所以租戶邊界仍是被攻擊者控制的值決定。詳見 F2/F3。
客戶機房裡裝了一個代理程式(agent),負責在客戶內網跑弱點掃描。它要做事,得先跟雲端拿兩樣東西:要掃的源碼壓縮包、檢測設定檔。這一棒檢查三件事:
agent_file_access_service.py)。掃描目標 ~/Projects/Jedicogy/module/jedi-python-package(掃描根目錄是整個 monorepo,範圍鎖在 13 個檔),revision 651e33c2(branch feature/review,工作區有未提交改動故 stamp 標 -dirty,該改動是刻意的套件路徑覆寫、與掃描內容無關),mode scan,effort low,focus attack-surface。範圍=jedi-detection 的代理程式相關 11 檔,加上重疊帶入 jedi-remote-agent 的 2 檔(任務狀態的 domain service 與 repository),檔數啟動前已核對為 13。
13 個檔以單一元件讀完——低強度跑法不做元件盤點、不做威脅建模、不跑額外的廣度掃描,completenessCheckOutcome 為 not-applicable(範圍掃描本就不適用)。這份報告完全不能拿來說 monorepo 其他地方沒事,連 jedi-detection 非代理程式相關的檔都不在內。
派出兩位研究員、兩位都回報。驗證跑一輪,6 個候選去重後 5 個,15 票全投出,沒有候選遺失、沒有候選未被審、沒有候選被交到下一輪。一個候選被 2:1 駁回(測試連線的明文退路,見下方「被駁回的候選」)。
有兩條發現的起點在範圍外:F2 與 F3 的路由在主專案(compliance-manager-be/api/remote_agent/routes/agent_file_route.py 與 core/plugins/remote_agent.py),F4 的請求入口在 jedi-remote-agent 自己的 route 與 app service。研究員與檢查員為了確認整條路徑有讀這些檔,但那些檔沒有被稽核,不能算進覆蓋率。F1 更是完全在範圍外(jedi-issue),是研究員在 repo 內工作時順手撈到的。
這份報告沒有逐檔紀錄:coverage.research 是 null,工具沒留下「哪位研究員把哪個檔讀到什麼程度」的帳。下方「卡片重點逐項人工查證」段由首腦逐項開檔補查,並標明哪些是工具報的、哪些是人工查的。
工具沒有實際執行任何程式碼:沒跑測試、沒發請求、沒示範攻擊。四條發現全部是讀原始碼、讀 git 歷史、以及對 DEV 資料庫做唯讀查詢推出來的。
stamp CLAUDE-SECURITY-REVISION-651e33c20870-dirty.json 蓋的是 status: unverified,拒收理由原話:1 finding(s) were refused at render and are absent from this report: F1。
這不是面板失敗,是渲染那一步退件。 與 R1b 同一種型態(交接文件自檢第 6 題的第三種):F1 指的是 git 歷史裡的檔案,現在的工作目錄已經沒有它,渲染器的規則認不得歷史檔,就把那一條退掉、整份蓋 unverified。面板本身完整跑完、15 票全投出,F1 自己是 3:0 通過的。機器可讀的 JSONL/SARIF 只收了 3 條,人讀的 markdown 報告四條全在。
| # | 嚴重度 | 這是什麼問題(白話) | 出事會怎樣 | 要先有什麼才打得到 | 位置 | 修正卡 |
|---|---|---|---|---|---|---|
| F1 | 中等 | 兩把外部平台的存取權杖被明文提交進版控,刪檔沒改歷史,每份 clone 都還拿得到 | 拿到的人能以該帳號身分讀(可能寫)該帳號能碰的所有 GitLab/GitHub 專案 | 任何一份 monorepo 的 clone,或 Nexus 上的舊套件檔 | jedi-issue/jedi_issue/.env:4(歷史檔) |
✅ 已處置(GitLab/GitHub 權杖已撤銷,2026-09-20;SUMMARY M01-6) |
| F2 | 中等 | 代理程式下載檔案的端點零認證,身分就是請求標頭自己寫的那行字;伺服器拿它繞過隔離查出租戶再切過去 | 不需任何帳號,把客戶的源碼壓縮包與檢測設定檔下載走,跨客戶也成立 | 連得到 API+知道一組代理程式編號與一個檔案編號+該任務還沒結束 | jedi-detection/.../agent_file_access_service.py:127 |
✅ 已修(CM-2052,與 CM-1595 同線) |
| F3 | 中等 | 同一條路徑的授權判定面:判定依據仍是攻擊者控制的那個編號 | 同上(跨客戶外洩源碼包與檢測設定檔) | 同上 | jedi-detection/.../agent_file_access_service.py:172 |
✅ 已修(CM-2052,同 F2) |
| F4 | 中等 | 任務狀態轉換不檢查回報者是不是當初派工的那台 | 不需任何帳號,把別人的掃描標成「成功」並塞進偽造的檢測結果,真結果之後被擋掉 | 連得到 API+知道一個還沒結束的任務編號 | jedi-remote-agent/.../agent_task_domain_service.py:93 |
✅ 已修(CM-2052;原卡 CM-1595 作廢) |
jedi-issue,不在任何一棒範圍內)這是什麼問題。 一個開發者用的 .env 設定檔,裡面放著 GitLab 的存取權杖(glpat- 開頭)與 GitHub 的存取權杖(ghp_ 開頭),在 2025-04-16(commit b740989)被提交進版控,直到 2026-07-25(commit d6a8d09)才刪掉。但刪的只是檔案,沒有改寫歷史——git merge-base --is-ancestor d6a8d09 HEAD 為真,且 git ls-tree d6a8d09^ 仍解得出那個 blob,所以每一份 clone 都還帶著這兩把明文鑰匙。兩個值都符合各自平台的權杖格式、長度正確(26 與 40 字元),不是佔位符。
出事會怎樣。 拿到的人就以那個帳號的身分持有兩個平台的 API 存取權:能讀該帳號碰得到的所有專案源碼,若權杖範圍含寫入,還能往那些專案裡塞程式碼。暴露窗口約十五個月。更麻煩的是刪除那筆 commit 自己的訊息寫著該檔曾被打包進發佈的 wheel,所以值不只在 git 裡,也在內部 Nexus 上的舊套件檔裡。
要先有什麼才打得到。
jedi-issue 套件檔。d6a8d09 是 HEAD 的祖先,blob 解得出來。在哪裡。 jedi-issue/jedi_issue/.env:4(GitLab)與同檔第 8 行(GitHub),存在於 d6a8d09^ 之前的歷史。
怎麼修。 先去 gitlab.com 與 github.com 撤銷這兩把權杖,並調閱兩個帳號自 2025-04-16 起的稽核紀錄——撤銷才是有效的控制,因為值已經散佈在每一份 clone 與已發佈的套件檔裡,清歷史是次要動作。程式那側維持既有的「呼叫端注入」寫法(get_gitlab_client(private_token=...)、get_github_client(token=...)),拿掉 os.getenv 的退路,並加一個提交前的密鑰掃描,讓 glpat-/ghp_ 這類字串不可能再被提交。
驗證。 3/3 三位檢查員一致確認成立。研究員原報高風險,面板降為中等(三票分別給中等、高、中等)。
首腦核對註記與判定。 ⚠️ 越界發現(檔案屬 jedi-issue,不在 FR-077 任何一棒的範圍內),但屬實。與 R1b 的 F2 是同一份歷史檔的不同兩把鑰匙——R1b 那把是寫死在 .py 程式碼裡的 GitLab 權杖(總表第 83 項),這裡是 .env 裡的 GitLab+GitHub 兩把。處置:併進總表第 83 項,把描述從「一把」擴為「同一支套件的歷史裡至少三把」,撤銷動作一次做完。 不另計新項。
這是什麼問題。 客戶機房的代理程式來雲端下載「這次任務要掃的源碼壓縮包」時,雲端唯一用來判斷「你是哪一台」的依據,就是請求標頭裡的 X-Agent-Uid 這一行字——那是請求自己寫的,誰都能寫。而且這個端點完全沒有掛任何認證:主專案的路由 agent_file_route.py:48 只有文件註解與注入兩個裝飾器,沒有 jwt_required,掛載處 core/plugins/remote_agent.py:132 也是裸掛,繞過了套件自己那張有守門的路由表。
伺服器拿到這行字之後做兩步:先提權(繞過資料庫的客戶隔離)去查「這個編號是哪一家客戶的」,再切換成那一家的身分去撈檔案。所以租戶邊界是由攻擊者填的那個值決定的。
出事會怎樣。 不需要任何帳號、任何憑證,就能把客戶上傳的源碼壓縮包與檢測設定檔下載回來,連同檔名、大小、SHA-256 摘要。因為租戶是從攻擊者指名的那一列查出來的,填別家客戶的代理程式編號,就讀到別家客戶的檔——跨客戶成立。
要先有什麼才打得到。
/api/1.0/agents/files/<uid>。落地版的對外入口是 nginx,而出貨的三個設定檔(前端 repo 的 nginx.onprem.conf、nginx.conf、nginx.e2e.conf)一行 ssl_verify_client 都沒有(首腦 2026-09-16 重新 grep 確認,連 scripts/installer/ 也沒有),所以程式註解裡「靠 mTLS 在上游證明身分」這個前提,在出貨的部署裡根本沒有被執行。在哪裡。 jedi-detection/jedi_detection/app/service/agent_file_access_service.py:127,AgentFileAccessService.get_file_for_agent。上游路由在主專案 api/remote_agent/routes/agent_file_route.py:48、掛載在 core/plugins/remote_agent.py:132。
怎麼修。 不要再把請求標頭當成身分聲明。改成要求雲端本來就會簽的那種短效通行證(jedi_remote_agent.common.agent_auth.jwt_util 簽的 RS256 JWT),驗 aud 是否等於宣稱的代理程式編號、驗 bound_fp 是否等於存檔的裝置指紋,代理程式編號從驗過的通行證裡取,不要從標頭取。如果部署上真的打算靠 mTLS,那對外入口就必須真的開起來(ssl_verify_client on 加內部 CA),並把驗過的身分傳進應用層。在這兩者其中之一存在之前,這個端點等於沒有認證。
驗證。 3/3 三位檢查員一致確認成立,嚴重度中等。
首腦核對註記與判定。 屬實,逐檔開過,三處都對得上:路由 :48 確實只有 @doc 與 @inject;_tenant_id_for_agent(:136-157)確實在 elevated_readonly_scope() 裡查 remote_agents;get_file_for_agent(:127)確實拿查出來的租戶去 tenant_context。
🔴 這條直接回答卡片重點①,而且答案比卡片預想的更寬。 卡片說「租戶由伺服器查出、resolver 跑在受隔離身分下,那一半已成立;要驗的變成同租戶內冒充」——但實查的結論是:查租戶那一步用的是攻擊者給的編號、且刻意繞過隔離,所以跨租戶並沒有被堵住。 程式的 docstring 辯護「指認成別台也只拿得到那台名下任務的檔」這句話本身是對的,但它默認了「別台」只會是同一家客戶的別台——實際上填哪一家的編號,就切到哪一家。卡片內「不要引用跨租戶已堵」這句提醒是正確的,本棒實證了它。
與 R1 的關係:R1 報的是註冊/心跳/ack/result 四個控制面端點零認證,這個檔案下載端點是第五個,R1 沒掃到(它在 jedi-detection 且路由留在主專案)。同一根因、不同端點、不同資料(前者洩掃描工具帳密,這裡洩客戶源碼),算新發現、不算重複。
這是什麼問題。 與 F2 是同一條請求路徑,差別在報的位置:F2 報在「查租戶」那一步,F3 報在「決定給不給」那一步。授權判定長這樣——逐一問兩個判定器,任一個點頭就放行;判定器問的是「這個檔案編號,是不是這台代理程式名下還沒結束的任務正在引用的?」。問題在於「這台」是誰,仍然是攻擊者在標頭上寫的那個編號。
出事會怎樣。 跨客戶洩漏源碼壓縮包與檢測設定檔。因為查租戶那一步跑在提權範圍裡(把 app.is_super_admin 設成 t,等於暫時擁有超級管理員視角),指名任何一家客戶的代理程式,就取得那一家的租戶身分。
要先有什麼才打得到。 與 F2 相同:連得到那個端點(無認證裝飾器)、知道一組代理程式編號與一個 upload_files 編號、目標檔案正被該代理程式進行中的任務引用、部署未真正啟用 mTLS 客戶端驗證。
在哪裡。 jedi-detection/jedi_detection/app/service/agent_file_access_service.py:172,AgentFileAccessService._get_file_for_agent(提權查詢在 :147-151)。
怎麼修。 與 F2 同一個修法:在 agent_uid 被用於任何判斷之前,先把請求綁到一個經密碼學驗證過的身分——短效通行證(驗 aud == agent_uid、iss、exp、bound_fp),或由代理伺服器傳下來的已驗證客戶端憑證;當 AGENT_AUTH_MODE=full 卻沒帶證明時直接拒絕。
驗證。 2/3 通過。異議的那位檢查員認為第 172 行是防護而不是破口——他的論點是:這一行如果兩個判定器都不認,就回 404,所以它擋住了「任意檔案讀取」。這個觀察是對的,但它針對的是「能不能拿任意檔」,而 F2/F3 講的是「能不能冒充成別人去拿他名下的檔」,兩者不衝突。研究員原報高風險,面板降為中等(兩張確認票都給中等)。因為只有 2 票通過,confidence 封頂在中等。
首腦核對註記與判定。 屬實。與 F2 是同一條路徑的兩個切面,開修正卡時併成一條處理、不要開兩張。 保留兩條分別呈現的理由:F2 說明「租戶怎麼被攻擊者選定」,F3 說明「授權判定為何攔不住」,修的時候兩處都要動(查租戶前先驗身分,判定時用驗過的身分)。
這是什麼問題。 代理程式跑完掃描後,會回報「這個任務做完了、結果在這裡」。接收這個回報的兩個端點(/agents/tasks/<uid>/ack 與 /agents/tasks/<uid>/result)在路由表裡標著 needs_admin=False,而掛載函式只對標著 True 的項目加守門——所以這兩個端點一個裝飾器都沒有。請求一路流進 update_status,那裡只檢查兩件事:任務存在嗎、這個狀態轉換合法嗎。從頭到尾沒有任何一步檢查「你是不是當初被派這個工的那台」。
出事會怎樣。 不需要任何帳號,就能把別家客戶的掃描任務標成「成功」,並附上自己挑的結果參照;或標成「失敗」,讓真正的掃描結果消失。更進一步:偽造的結果參照裡的檔案編號,之後會被拼進代理程式的取檔路徑(detection_result_handler._fetch_blob 的 path = f"/blob/{upload_uid}"),取回來的內容會被當成合規稽核的證據掛進案子裡。真正的代理程式稍後回報時,會因為「任務已在終態、轉換不合法」而被擋掉——假的先到就贏。
要先有什麼才打得到。
/api/1.0/agents/tasks/<uid>/ack 或 /result(兩者都無認證)。jedi_remote_agent/api/routes/remote_agent_route.py 與 app/service/agent_task_service.py,同套件)。在哪裡。 jedi-remote-agent/jedi_remote_agent/domain/agent_task/service/agent_task_domain_service.py:93,AgentTaskDomainService.update_status。路由表在同套件 api/routing.py:39-40,掛載邏輯在 :95。
怎麼修。 把驗證過的代理程式身分(短效通行證的 aud/bound_fp,或已驗證的客戶端憑證)傳進 AgentTaskService.ack_task 與 receive_result,在呼叫 update_status 之前先斷言 task.agent_id 與它相符。或者把「歸屬」變成 update_status 的必要參數,讓任何呼叫端都不可能轉換一個不屬於它的任務。
驗證。 3/3 三位檢查員一致確認成立,嚴重度中等。
首腦核對註記與判定。 ⚠️ 重疊帶入檔(R2a 也在掃),且這兩個端點 R1 已經報過——R1 的 F2+F5「ack/result 無認證、不查歸屬」,修正卡 CM-1595 已開。本條標「重複 R1 F2/F5」,不另計新項。
但這次有增量價值,值得寫進 CM-1595:R1 是從端點那一側看到的,本棒是從狀態機那一側看到的,證實了「即使有人之後在端點補上認證,update_status 這一層仍然不檢查歸屬」——所以修法不能只在端點掛裝飾器,必須把身分一路傳到 domain service 做歸屬斷言,否則第二個呼叫路徑(例如未來新增的批次補登、維運工具)會再次繞過。
卡片重點⑦問的「授權窗口能不能被延長」:見下方人工查證段。
測試連線的明文退路(agent_probe_client.py:83)。研究員報的是:當代理程式認證處於關閉狀態(mode=none)時,_post 走的是裸 httpx.post,沒有加密驗證也沒有帶通行證,而請求內容裡帶著檢測工具的明文帳密。
兩位檢查員駁回,理由一致且已由首腦覆核為正確:安裝程式強制把 AGENT_AUTH_MODE 設成 full——scripts/installer/install.sh:1657 在全新安裝時寫入,升級時走 :1645 的分支(已有就保留、沒有就補上),.env.sample:157 也是 full。所以出貨的部署不會停在 none。
首腦補充兩點(駁回成立,但要留紀錄):
none(config/config.py:257 的 os.getenv("AGENT_AUTH_MODE", "none")),安全是靠安裝程式補上去的,不是靠程式本身。開發環境、手動部署、或任何沒跑過 install.sh 的環境,都會停在關閉狀態。install.sh:1645 的分支是「設定檔裡已經有這個值就保留現值不覆寫」——如果某台機器的設定檔在更早以前被寫成 none,升級不會把它改回 full。這是設計上刻意的(不覆寫客戶設定),但意味著「升級過的機器一定是 full」這句話不成立。兩點都不推翻駁回(出貨路徑確實是 full),列在這裡供修正卡評估時參考。
🔴 低強度只跑兩位研究員,卡片列的七個重點只碰到其中三個(①②⑦的一部分)。其餘由首腦回頭開檔、連 DEV 資料庫唯讀查證補上。
結論:成立,且範圍比卡片預想的更寬(跨租戶也成立)。 詳見 F2 首腦註記。
補答卡片問的「那台名下的檔本身值不值得偷」:兩個判定器認的分別是 params["_source_file"].uid(客戶上傳的源碼壓縮包)與 params["_profile"].uid(檢測設定檔)。源碼包是客戶自己的程式碼,價值明確。門檻是「知道一台代理程式的編號」,而編號出現在:代理程式管理頁(任何登入者可見)、代理程式主機自己的設定檔、後端請求 log(產品自己的待辦紀錄已載明 log 會印明文請求內容)、以及註冊與心跳的回應。
結論:不成立,這一點程式做對了。
逐行核對 _tenant_id_for_agent(:136-157):編號為空 → 404;查無 → 404;狀態為 revoked → 404;tenant_id 為 None → 404;提權範圍不可用(ElevatedSessionUnavailable)→ 404。往下走之後,判定器全部不認 → 也是 404(:172-177)。五種失敗回同一個錯誤碼 DETECTION_TOOL_AGENT_FILE_NOT_FOUND,不區分「不存在」與「無權限」,docstring 明文寫著這是刻意的。
時間差方面:查無的路徑比通過的路徑少跑一次租戶切換與最多兩次任務查詢,理論上存在時間差,但要用它來區分「這個編號是不是一台活的代理程式」需要穩定的計時樣本,而真正的差異(一次 DB 查詢)遠小於網路抖動。不構成實用的探測管道,不另報。
params 可控性(工具未報,人工查證)結論:這條路走不通,但理由不是守門,是「任務不是使用者建的」。
_resolvers 確實是一個清單、兩個判定器都比對 params[key].uid(:77-93)。卡片問「有管理面權限的人能不能把任意 upload_files.uid 塞進某張任務的 params」——實查:agent_tasks 的 params 不是由任何對外 API 直接寫入的,它是在檢測工單展開時由服務端組出來的(源碼包編號來自該次執行綁定的上傳檔、profile 編號來自綁定的檢測設定),沒有一個端點接受使用者提供的 params 整包。
所以「自己給自己開授權」需要先有能直接寫 agent_tasks.params 的能力,那已經是資料庫層級的存取,不是這條路徑的問題。不另報。
但順帶更正卡片背景一處(見下方⑥)。
結論:駁回正確(出貨是 full),但有兩個值得記錄的邊角,已寫在「被駁回的候選」段。
逐項回答卡片問的:
_DisabledSettings 確實等同不加認證(agent_auth.py:47-51,enabled = False、mode = "none"),警告只發一次(_warned_not_configured 旗標,:57-68)。agent_probe_client._post:83 與 agent_cancel_client._post:68 都是 httpx.post(url, ...) 裸呼叫、不驗憑證、不帶通行證;且兩支開頭那道「認證開啟時必須是 https」的檢查(probe :45-47、cancel :44-46)本身就掛在 enabled 之下,所以關閉狀態下連 https 都不強制——可以是明文 http。打的位址是 agent.base_url,那是代理程式自己註冊時報上來的,所以確實存在伺服器端請求偽造的面(與 R1 F4 check_health 同型,修正卡 CM-1596)。full:有,install.sh:1657(全新安裝)與 :1645 的保留分支(升級)。但程式碼預設值是 none(config/config.py:257)。結論:兩支形狀完全一致,認證開啟時做對了,關閉時兩支都退化成裸明文。
| 檢查項 | agent_probe_client.py |
agent_cancel_client.py |
|---|---|---|
| 開啟時走 mTLS | ✅ build_cloud_mtls_context(settings)(:92) |
✅ 同(:77) |
| 開啟時簽通行證 | ✅ jwt_util.mint(...)(:86-90) |
✅ 同(:71-75) |
| 關閉時 | ⚠️ 裸 httpx.post(:83) |
⚠️ 裸 httpx.post(:68) |
| 位址怎麼組 | agent.base_url + 固定路徑,base_url 由代理程式自報 |
同 |
| 逾時 | ✅ 70 秒(有設,且註解說明理由) | ✅ 30 秒 |
| 例外有沒有被吞 | ⚠️ httpx.HTTPError 與 ValueError 都被接住轉成 success=False,但有寫 log(warning/error),呼叫端拿得到失敗訊號 |
同,且檔頭註明「取消失敗必須讓呼叫端知道」 |
FR-039 那條規矩(所有雲端→代理程式的呼叫要走 mTLS+JWT)兩支都遵守了,卡片擔心的「第三處裸 httpx」不成立——兩支的裸呼叫都只在認證關閉時才走到,與既有的 check_health 同型。沒有新發現。
job_execution_detection_tool_agent 這條線(工具未報,人工查證+DEV 唯讀查詢)結論:隔離做好了,但 soft-ref 的疑慮成立、影響有限。
TenantScopedMixinModel(model/job_execution_detection_tool_agent.py:12)。DEV 唯讀查詢(2026-09-16):job_execution_detection_tool_agents 的 relrowsecurity = t(隔離已開、未開強制模式),有一條 jedta_tenant_isolation policy 涵蓋全部操作(ALL),內容是「超級管理員逃生門 OR 本客戶」。隔離是有的。list_by_binding_ordered 的 tenant_id 是選填參數,沒傳就不加這個條件——但因為資料庫層隔離已開,沒傳時仍由 RLS 擋住,不是破口。agent_uid 是 soft-ref:model 的欄位註解明說「跨 schema 不建外鍵,有效性驗證由應用層負責」。跨租戶的編號塞進來會怎樣:這張表本身有租戶隔離,所以塞進來的那一列會被記在攻擊者自己的租戶下;展開成工單時會去查那台代理程式,而查詢同樣受隔離擋住 → 查不到 → 工單派不出去。結果是自己的工單壞掉,不是拿到別人的東西。不另報。🔴 順帶更正卡片背景一處:卡片寫「跨 arc 總表第 47 項:upload_files 零隔離、無租戶欄」。DEV 唯讀實查(2026-09-16):upload_files 有 tenant_id 欄、relrowsecurity = t、四條 policy(select/insert/update/delete)齊全,select 的條件是「超級管理員 OR 本客戶 OR storage_scope='system'」。「零隔離」這個描述已經過期(推測是 2026-09-16 當天稍早的 commit a48b41cc「背景與系統上傳補租戶身分,加 upload_files RLS 守衛測試」補上的)。總表第 47 項需要重新確認狀態。
這不影響 F2/F3 的成立——那兩條的問題是「租戶身分被攻擊者選定」,隔離本身有沒有開不是重點,因為攻擊者是合法地切進了目標租戶。
結論:查詢條件做對了;授權窗口確實可被延長,但延長者只能是那台代理程式自己。
list_for_agent_by_status(agent_id, tenant_id, status) 真的同時用兩個條件嗎:✅ 是。agent_task_domain_service.py:146-155 把三個值一起包進 AgentTaskQueryEntity(agent_id=..., tenant_id=..., status=...) 交給 get_all_by_fields,三個條件都會進 WHERE。而且 agent_tasks 表的 RLS 也開著(DEV 實查 relrowsecurity = t,一條 agent_tasks_tenant_isolation 涵蓋 ALL)。這一步不是破口。
_ACTIVE_TASK_STATUSES 的授權窗口能不能被延長:窗口=pending/dispatched/running 三種狀態(agent_file_access_service.py:56)。要讓任務永遠不進終態,就是永遠不回報 succeeded/failed/cancelled——而回報是那台代理程式自己的動作,它只要不回報,窗口就一直開著。程式裡沒有看到任何逾時清理機制把久未回報的任務強制收尾。
但這個「延長」的價值有限:窗口開著只代表「該任務引用的那幾個檔」持續可下載,不會擴大到別的檔。而且配合 F4,任何人都能替它回報終態把窗口關掉(這反而是另一個問題)。不另報為獨立發現,但建議寫進 F2/F3 的修正卡:加一個任務逾時機制,讓授權窗口有上界。
兩位研究員讀完 13 個檔案、提報 6 個候選(去重後 5 個),接著由固定的三位檢查員(分別從「打不打得到」「影響有多大」「有沒有防護擋住」三個角度)投票,一輪共 15 票全數投出。F1、F2、F4 三票全過;F3 兩票過、一票異議(異議在「第 172 行算破口還是防護」,不在程式路徑本身),因此 F3 的把握度封頂在中等。一個候選被 2:1 駁回並經首腦覆核成立(測試連線的明文退路,被安裝程式的預設值擋住)。面板把 F1 與 F3 的嚴重度從研究員原報的「高」降為「中等」,本報告採用面板的最終評級,不還原。
驗證章 unverified,原因是渲染那一步退掉了 F1(它指向的檔案只存在於 git 歷史、不在現在的工作目錄),不是面板失敗;機器可讀格式只含 3 條,本報告四條齊全。
沒有任何程式碼被執行:沒跑測試、沒發請求、沒示範攻擊。所有結論來自讀原始碼、讀 git 歷史、以及對 DEV 資料庫的唯讀查詢。
原始工具產出:~/Projects/Jedicogy/module/jedi-python-package/CLAUDE-SECURITY-20260916-134333/ (CLAUDE-SECURITY-RESULTS.md 為工具原文,本報告為首腦整理版,含逐項人工查證。)