FR-097 · 需求索引 · 本頁由 build 掃資料夾生成
✅ 兩棒全數掃完並驗收,arc 收尾(2026-09-16);發現已全數處理——M09 七件已修,隨 1.21.0 出貨(母卡 CM-1810、子卡 CM-1811~1812 一次建完連號)。套件 90 檔,按「改設定會怎樣/誰讀得到」切兩棒共 82 檔:L1 日誌轉送全鏈(39,jedi)✅ 已驗 → L2 API 操作記錄全鏈(43,jedi)✅ 已驗。共 5 條發現(2 高 3 中):一個客戶的管理員可以把全公司的日誌改送到他自己的機器(高)/匯出的 Excel 檔可被種公式炸彈、不必登入就能種(高)/整條路沒加密(中)/可塞偽造紀錄(中)/兩張日誌表保存期限完全沒生效、已實查確認早就發作(中)。這支從來沒掃過。風險形狀特殊——日誌本身就是最敏感的資料(依跨 arc 總表第 22 項,登入密碼與憑證會原文寫進日誌),所以「日誌要送去哪裡」這個設定被改掉,等於把含密碼的整包資料導向攻擊者的機器。
兩棒都已掃完並驗收(2026-09-16 收口);5 條發現已全數處理——對應總表 M09 七件全數已修,隨 1.21.0 出貨。
這支套件管兩件事:把系統日誌即時送到外部伺服器(客戶自己的資安監控系統),以及記錄誰對系統做了什麼操作(供事後稽核查閱)。
為什麼值得掃,一句話:日誌本身就是最敏感的資料。依跨 arc 總表已登記的第 22 項,目前使用者的登入密碼與登入憑證會原文寫進日誌。所以「日誌送去哪裡」這個設定一旦被改掉,等於把含有密碼的整包資料持續、悄無聲息地導向攻擊者的機器。
首腦開卡時已開檔核出的最大疑點(未經三位檢查員投票驗證,寫進卡當錨點):
🔴 改「日誌送去哪裡」的權限是「客戶層級」,但那張設定表是全系統共用一列。 開發環境實查確認:log-forwarding.update 這個權限的平台層旗標是 false——代表它是客戶層級權限,而依租戶開通的邏輯(把權限全集扣掉平台層的、其餘全部發給該客戶的預設管理員角色),每開一個新客戶就自動發給那家的管理員。但設定表的資料只有「全域那一列」(客戶欄位是空值,建表腳本檔頭明寫「第一版是系統層全域一份」)。
這與 2026-09-15 剛在 FR-096 第 2 棒查到的第 74 項是完全相同的形狀(客戶的管理員可以改掉全公司的登入規則),而那一條已經證實成立、並經開發環境實查佐證。
| 棒 | 卡 | 範圍 | 檔數 | 狀態 | 首腦驗收 |
|---|---|---|---|---|---|
| L1 | CM-1811 | 日誌轉送全鏈(jedi 套件,含 2 支 SQL) | 39 檔 | ✅ 掃完,工具 2 條中風險 + 首腦補查 1 條高風險 報告|驗證章 verified |
✅ 已驗(2026-09-15) 兩條屬實不改判;工單重點①工具沒碰,首腦補查證實成立(高) |
| L2 | CM-1812 | API 操作記錄全鏈(jedi 套件,含 2 支 SQL) | 43 檔 | ✅ 掃完,工具 1 條高風險 + runner 五項重點逐項人工補查、命中 1 個已知缺口 報告|驗證章 verified |
✅ 已驗(2026-09-16) F1 屬實維持高;首腦實查更正分區缺口的時間點(早已發作) |
一次只派一棒,每棒獨立驗收完才派下一棒。合理停損點在第 1 棒之後——L1 涵蓋「誰改得動日誌去向」這個真正的風險面;L2 的守門看起來紮實、且它的主要問題已登記在總表第 23 項。
⚠️ 套件根層還有 6 支檔案兩棒都不含(jedi_api_log/__init__.py 與 plugin/ 五支)。那幾支定義的是「宿主必須填哪些插槽」,屬接線層,會併進之後與 jedi-asset 合併的宿主接線棒一起掃。這是刻意的、不是漏掉。
結論:工具報的兩條都屬實。但工單標為「本棒最重要」的那個疑點工具完全沒碰,首腦補查後證實成立——而且它比工具報的兩條都嚴重。本棒實際是 3 條(1 高 2 中)。
首腦親自開檔核對(不是聽轉述):
| 項目 | 結果 | 白話 |
|---|---|---|
| 驗證章狀態 | verified,無拒收原因 |
完整跑完 |
| 候選 → 去重後 | 2 → 2 | 沒有重複的 |
| 投票數 | 6(2 條 × 3 位檢查員) | 全部投完,零漏投 |
| 未審候選 / 中斷 / 被降級 | 0 / 0 / 0 | 乾淨 |
| 研究員 派出/回報 | 1 / 1 | — |
| 耗時 | 5,427 秒(1 小時 31 分) | — |
| 掃描的程式版本 | 9b35885,工作區乾淨 |
掃的就是那一版 |
| 範圍 | 39 檔,與工單逐檔對上 | ✅ |
工單開卡時就寫明「本棒最重要的一條」是「客戶層級的權限改到全系統共用的設定」。工具兩條發現都沒有碰它——它報的是「送出去的資料本身」(沒加密、可塞假紀錄),不是「誰改得動目的地」。
首腦依工單補查,四項證據全部對上:
| 查了什麼 | 結果 |
|---|---|
| 權限是不是客戶層級 | ✅ log-forwarding.update 的平台層旗標是 false |
| 誰實際持有 | ✅ 9 個客戶裡有 8 個非總部客戶的管理員角色持有(開發環境實查,18 筆) |
| 寫入落在哪一列 | ✅ log_forwarding_app_service.py:64 把客戶欄位寫死成空值,交給 save_global();資料層 log_forwarding_setting_repo_impl.py:29 固定撈客戶欄位為空的那一列 |
| 資料表實況 | ✅ 開發環境 config.log_forwarding_settings 只有 1 筆,客戶欄位是空的 |
→ 一個客戶的管理員,可以把全公司(含平台管理員自己)的日誌,改送到他自己的機器上。 已登記為跨 arc 總表第 75 項,高風險。
這與 FR-096 第 74 項是同一種病的第二個實例(客戶層級的權限寫進全系統共用的設定),該案已證實成立。兩者建議合開同一張工單。
| 發現 | 嚴重度 | 首腦核對了什麼 | 判定 |
|---|---|---|---|
| F1 整條路沒有加密 | 🟡 中(維持) | 開 forwarder.py:181 確認 syslog 走 stdlib 的一般連線、gelf 那半同樣;整個轉送資料夾 grep ssl/tls/cert 零命中 |
✅ 屬實 |
| F2 沒過濾換行,可塞偽造紀錄 | 🟡 中(維持) | 開 forwarder.py:232 確認組出那一行時直接接上訊息內容、沒清控制字元;比對 gelf_handler.py:95 確實用 split("\n", 1)[0] 限制在第一行——syslog 這半沒跟上 |
✅ 屬實 |
F1 為什麼維持「中」而不升級:三位檢查員 2:1 通過,持保留的那位認為「目的地是管理員設的、攻擊者不能直接觸發」。首腦同意維持中等——它要成立得先有人站上網路路徑,門檻高於第 75 項。但它是第 75 項的放大器:改掉目的地之後,不必解密就能直接讀。
F2 值得一提:三位檢查員一致通過,其中一位實際送出帶編碼換行的請求、驗證它真的能一路傳到格式化那一行——這是本棒唯一一條有實際操作驗證、非僅讀程式碼推論的發現。
🔵 首腦另外查到一個報告沒寫的細節:修 F2 時不能只改 syslog 那半。gelf 那半雖然把 short_message 限制在第一行,但完整訊息欄位仍原樣帶出——只修一邊等於沒修乾淨。已寫進總表第 77 項。
這一棒的價值不在單條,而在它們疊起來:
改得動目的地(75,高)→ 路上完全沒加密、不必解密就看得到(76,中)→ 還能塞偽造紀錄混淆事後追查(77,中)
而且與跨 arc 總表第 22 項直接相扣——日誌裡有登入密碼與憑證的原文,所以這條管道外洩的就是密碼本身。
| 項目 | 狀況 |
|---|---|
| 報告 | ✅ runner 自行交付(scan-L1-log-forwarding.md) |
| commit | ✅ runner 自行 commit(207a29ab) |
| Notion 子卡回寫 + 狀態改「修正待驗證」 | ✅ runner 自行完成 |
| 修正卡 | ✅ 已處理——三條登記於總表第 75/76/77 項,後由 FR-114 修正線修完(M09,1.21.0 出貨) |
✅ 交付四件齊全——本計畫第九次做滿。
結論:工具那條屬實、維持高風險。runner 的五項人工補查全部覆核通過。但有一處事實要更正——而且方向是往嚴重的那邊。FR-097 兩棒收尾。
| 項目 | 結果 | 白話 |
|---|---|---|
| 驗證章狀態 | verified,無拒收原因 |
完整跑完 |
| 候選 → 去重後 | 1 → 1 | — |
| 投票數 | 3(1 條 × 3 位檢查員) | 三位一致通過 |
| 未審候選 / 中斷 / 被降級 | 0 / 0 / 0 | 乾淨 |
| 耗時 | 4,411 秒(1 小時 14 分) | — |
| 掃描的程式版本 | cdb0d0f4,工作區乾淨 |
與 L1 不同版,見下 |
| 範圍 | 43 檔,與工單逐檔對上 | ✅ |
掃描版本與 L1 不同要說明:L1 掃的是 9b35885,L2 是 cdb0d0f4。首腦比對兩版之間的唯一一個 commit(jedi-iam 發版)沒有動到本棒範圍內任何一個檔,零漂移,結果有效。
首腦逐段開檔核對:
api_log_service.py:89 確認寫進 Excel 之前只過了一支清理函式,而那支(excel_util.py)只清掉看不見的控制字元,公式開頭字元完全沒管——屬實。common/middleware/app_mw.py:40-42 確認記錄攔截器確實在任何權限檢查之前執行,所以種公式不需要登入——屬實。🔵 首腦補充一項報告沒寫的事實:匯出的資料格式共 17 個欄位,其中網址、查詢字串、請求內容、回應內容、訊息、瀏覽器識別字串六個都吃得到外部輸入——不是只有瀏覽器識別字串那一欄。這讓「要修在出口、不要逐欄補」這個建議更站得住腳。
報告寫「9 月份的分區已經建好、現在還沒出事、10 月才會開始掉進備用表」。開發環境實查後不成立:
| 查了什麼 | 實際結果 |
|---|---|
| 操作記錄的分區 | 只有 5 月到 8 月,沒有 9 月 |
| 操作記錄的備用表 | 已堆 20,959 筆,全部是 9 月的資料(9/1~9/15) |
| 系統日誌的分區 | 同樣只有 5~8 月,沒有 9 月 |
| 系統日誌的備用表 | 已堆 87,471 筆,從 8/31 就開始 |
缺口不是「將要發作」,是「已經發作半個月了」。 判斷方向對、時間點錯——而錯的方向會讓人低估急迫性。
這個更正不改變它的定性(仍是既有已知缺口、不是本棒新發現,已登記在 docs/analysis/2026-07-07-known-pits-remediation-tracker.md),但讓它從「未來的風險」變成「當下的事實」。
還有一層要講:保存期限沒生效與跨 arc 總表第 22 項直接相扣——日誌裡有密碼與登入憑證的原文,清理機制失效等於那些原文無限期留著。
| 重點 | runner 的結論 | 首腦覆核 |
|---|---|---|
| ①匯出(範圍/筆數/暫存檔/檔名) | 全表匯出、無筆數上限、全走記憶體無暫存檔、檔名不吃輸入 | ✅ 屬實。無筆數上限值得留意——目前 12.6 萬筆還可控,但這支功能本身沒有防護 |
| ②查詢條件 | 全部非必填、送空回全表;排序有白名單 | ✅ 屬實,歸第 70 項那條線正確,沒有誤升級成新發現 |
| ③分區維護 | 從未被排程呼叫 | ✅ 根因屬實,但時間點被推翻(見上) |
| ④寫入來源遮罩 | 請求內容與回應有遮罩,網址/查詢字串/瀏覽器識別字串沒有 | ✅ 屬實,歸第 22 項同主題正確;這三欄未遮罩也支撐了 F1 的影響面 |
| ⑤選單權限綁定 | 讀取權限是平台層;兩段 SQL 都是冪等、不覆蓋客戶改過的值 | ✅ 屬實 |
五項全部覆核通過,只有③的時間點被推翻。
工具只碰到五個重點裡的一個,而且只碰到那一個重點的一個角度(匯出裡的公式注入)。其餘四個重點、以及匯出裡另外三個子問題,全部是 runner 人工補查出來的。
這與 L1 的教訓完全一致,而這次 runner 在掃描前就知道要這樣做。兩棒的報告都在可信度段誠實寫出「工具沒碰哪幾點」——往後每棒照做。
| 項目 | 狀況 |
|---|---|
| 報告 | ✅ runner 自行交付(scan-L2-api-log.md) |
| commit | ✅ runner 自行 commit(85d33336) |
| Notion 子卡回寫 + 狀態改「修正待驗證」 | ✅ runner 自行完成 |
| 修正卡 | ✅ 已處理——第 78/79 項後由 FR-114 修正線修完(M09,1.21.0 出貨) |
✅ 交付四件齊全——本計畫第十次做滿。
兩棒的分界是「改設定會怎樣」與「誰讀得到」:
為什麼不合成一棒:合起來 82 檔遠超過一棒的上限(投票成本=候選數×3 位檢查員,每位都從零讀檔),而且兩者的風險形狀完全不同,混在一棒會讓研究員把兩套判斷標準搞混。
總表 §4 的排名表把這支排在第 4 順位,理由寫「日誌轉送的目標設定裡包含帳密」——這句不成立。首腦開了建表腳本逐欄核對,那張表只有主機、埠號、通訊協定、開關這些欄位,沒有任何帳號密碼欄位。
所以這支的原始排序理由有一半是錯的。但仍值得掃,理由改成上面「一頁看完」講的那件事:風險不在「設定裡存了什麼」,而在「誰改得動、改了會把什麼導去哪裡」。
這三件首腦在開卡前已開檔核對,掃到請標「已知、非新發現」:
core/plugins/api_log.py:185-186 確實注入了「要登入 + 要有對應權限」,而且讀與寫分成兩個不同權限。研究員若只看套件會誤報成「無守門」,這是本 arc 最可能的誤報來源。| 項目 | 值 |
|---|---|
| 主 session 模型 | Opus 5 (1M context),不要用 Sonnet(研究員繼承主 session 的內容上限,Sonnet 跑不完會被工具的「180 秒沒動作就判當掉」砍掉重派) |
| effort | low(單一研究員掃全範圍+三位檢查員投票,不跑盤點、不做威脅建模) |
| focus | attack-surface(跳過測試碼、打包產物、文件) |
| 派工節奏 | 一次只派一棒,驗收完才派下一棒 |
.claude/skills/security-scan-lead/SKILL.mddocs/features/security-scan-consolidated/~/Projects/Jedicogy/module/jedi-python-package/jedi-log/main、BE repo main(2026-09-15 開卡時實查)以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。
| 文件 | 類型 | 標題 | 最後更新 |
|---|---|---|---|
| scan-L1-log-forwarding / md | 盤點證據 | L1 檢查結果:日誌轉送全鏈(jedi-log) | 2026-09-15 |
| scan-L2-api-log / md | 盤點證據 | L2 檢查結果:API 操作記錄全鏈(jedi-log) | 2026-09-15 |