FR-097 · 需求索引 · 本頁由 build 掃資料夾生成

FR-097 系統日誌轉送與 API 操作記錄(jedi-log)資安檢查

✅ 兩棒全數掃完並驗收,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 項,登入密碼與憑證會原文寫進日誌),所以「日誌要送去哪裡」這個設定被改掉,等於把含密碼的整包資料導向攻擊者的機器。

狀態:✅ 已完成 文件 2 份

🔴 一頁看完

兩棒都已掃完並驗收(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 合併的宿主接線棒一起掃。這是刻意的、不是漏掉。

L1 驗收(首腦,2026-09-15)

結論:工具報的兩條都屬實。但工單標為「本棒最重要」的那個疑點工具完全沒碰,首腦補查後證實成立——而且它比工具報的兩條都嚴重。本棒實際是 3 條(1 高 2 中)。

§1

工具的驗證章

首腦親自開檔核對(不是聽轉述):

項目 結果 白話
驗證章狀態 verified,無拒收原因 完整跑完
候選 → 去重後 2 → 2 沒有重複的
投票數 6(2 條 × 3 位檢查員) 全部投完,零漏投
未審候選 / 中斷 / 被降級 0 / 0 / 0 乾淨
研究員 派出/回報 1 / 1 —
耗時 5,427 秒(1 小時 31 分) —
掃描的程式版本 9b35885,工作區乾淨 掃的就是那一版
範圍 39 檔,與工單逐檔對上 ✅
§2

🔴 首腦補查:工單重點①證實成立,而且是本棒最嚴重的一條

工單開卡時就寫明「本棒最重要的一條」是「客戶層級的權限改到全系統共用的設定」。工具兩條發現都沒有碰它——它報的是「送出去的資料本身」(沒加密、可塞假紀錄),不是「誰改得動目的地」。

首腦依工單補查,四項證據全部對上:

查了什麼 結果
權限是不是客戶層級 ✅ 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 項是同一種病的第二個實例(客戶層級的權限寫進全系統共用的設定),該案已證實成立。兩者建議合開同一張工單。

§3

逐條核對(首腦自己開檔)

發現 嚴重度 首腦核對了什麼 判定
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 項。

§4

三條合起來是一條完整攻擊鏈

這一棒的價值不在單條,而在它們疊起來:

改得動目的地(75,高)→ 路上完全沒加密、不必解密就看得到(76,中)→ 還能塞偽造紀錄混淆事後追查(77,中)

而且與跨 arc 總表第 22 項直接相扣——日誌裡有登入密碼與憑證的原文,所以這條管道外洩的就是密碼本身。

§5

這份結果可信到什麼程度

  • 「這幾條是真的嗎」→ ✅ 可信。 工具兩條逐條開檔核對屬實;首腦補查那條有程式碼三處+開發環境兩項實查佐證。
  • 「是不是只有這幾條」→ ❌ 不可信,而且這一棒證明了為什麼。 最嚴重的一條工具完全沒提,是靠工單先寫明重點、首腦事後補查才撈出來的。這是快篩模式(單一研究員讀完 39 檔,不做盤點、不做威脅建模)的固有限制——工單的「重點看什麼」必須有人逐項回頭核,不能指望工具照著走。
§6

交付狀況

項目 狀況
報告 ✅ runner 自行交付(scan-L1-log-forwarding.md)
commit ✅ runner 自行 commit(207a29ab)
Notion 子卡回寫 + 狀態改「修正待驗證」 ✅ runner 自行完成
修正卡 ✅ 已處理——三條登記於總表第 75/76/77 項,後由 FR-114 修正線修完(M09,1.21.0 出貨)

✅ 交付四件齊全——本計畫第九次做滿。

L2 驗收(首腦,2026-09-16)

結論:工具那條屬實、維持高風險。runner 的五項人工補查全部覆核通過。但有一處事實要更正——而且方向是往嚴重的那邊。FR-097 兩棒收尾。

§7

工具的驗證章

項目 結果 白話
驗證章狀態 verified,無拒收原因 完整跑完
候選 → 去重後 1 → 1 —
投票數 3(1 條 × 3 位檢查員) 三位一致通過
未審候選 / 中斷 / 被降級 0 / 0 / 0 乾淨
耗時 4,411 秒(1 小時 14 分) —
掃描的程式版本 cdb0d0f4,工作區乾淨 與 L1 不同版,見下
範圍 43 檔,與工單逐檔對上 ✅

掃描版本與 L1 不同要說明:L1 掃的是 9b35885,L2 是 cdb0d0f4。首腦比對兩版之間的唯一一個 commit(jedi-iam 發版)沒有動到本棒範圍內任何一個檔,零漂移,結果有效。

§8

F1 匯出 Excel 公式炸彈:✅ 屬實,維持高風險

首腦逐段開檔核對:

  • api_log_service.py:89 確認寫進 Excel 之前只過了一支清理函式,而那支(excel_util.py)只清掉看不見的控制字元,公式開頭字元完全沒管——屬實。
  • 主專案 common/middleware/app_mw.py:40-42 確認記錄攔截器確實在任何權限檢查之前執行,所以種公式不需要登入——屬實。

🔵 首腦補充一項報告沒寫的事實:匯出的資料格式共 17 個欄位,其中網址、查詢字串、請求內容、回應內容、訊息、瀏覽器識別字串六個都吃得到外部輸入——不是只有瀏覽器識別字串那一欄。這讓「要修在出口、不要逐欄補」這個建議更站得住腳。

§9

🔴 首腦實查更正一處:分區失效早就發作了

報告寫「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 項直接相扣——日誌裡有密碼與登入憑證的原文,清理機制失效等於那些原文無限期留著。

§10

runner 五項補查,首腦覆核

重點 runner 的結論 首腦覆核
①匯出(範圍/筆數/暫存檔/檔名) 全表匯出、無筆數上限、全走記憶體無暫存檔、檔名不吃輸入 ✅ 屬實。無筆數上限值得留意——目前 12.6 萬筆還可控,但這支功能本身沒有防護
②查詢條件 全部非必填、送空回全表;排序有白名單 ✅ 屬實,歸第 70 項那條線正確,沒有誤升級成新發現
③分區維護 從未被排程呼叫 ✅ 根因屬實,但時間點被推翻(見上)
④寫入來源遮罩 請求內容與回應有遮罩,網址/查詢字串/瀏覽器識別字串沒有 ✅ 屬實,歸第 22 項同主題正確;這三欄未遮罩也支撐了 F1 的影響面
⑤選單權限綁定 讀取權限是平台層;兩段 SQL 都是冪等、不覆蓋客戶改過的值 ✅ 屬實

五項全部覆核通過,只有③的時間點被推翻。

§11

🔴 這一棒最有價值的不是發現,是做法

工具只碰到五個重點裡的一個,而且只碰到那一個重點的一個角度(匯出裡的公式注入)。其餘四個重點、以及匯出裡另外三個子問題,全部是 runner 人工補查出來的。

這與 L1 的教訓完全一致,而這次 runner 在掃描前就知道要這樣做。兩棒的報告都在可信度段誠實寫出「工具沒碰哪幾點」——往後每棒照做。

§12

交付狀況

項目 狀況
報告 ✅ runner 自行交付(scan-L2-api-log.md)
commit ✅ runner 自行 commit(85d33336)
Notion 子卡回寫 + 狀態改「修正待驗證」 ✅ runner 自行完成
修正卡 ✅ 已處理——第 78/79 項後由 FR-114 修正線修完(M09,1.21.0 出貨)

✅ 交付四件齊全——本計畫第十次做滿。

需求討論紀錄

§13

為什麼這樣切

兩棒的分界是「改設定會怎樣」與「誰讀得到」:

  • L1(日誌轉送,39 檔)——從設定頁的三支 API 一路到實際送出網路封包的背景程式,再到隨套件出貨的建表與授權 SQL。風險最高,先跑。
  • L2(API 操作記錄,43 檔)——查詢、匯出、按月分區的表結構、選單與權限綁定。收尾。

為什麼不合成一棒:合起來 82 檔遠超過一棒的上限(投票成本=候選數×3 位檢查員,每位都從零讀檔),而且兩者的風險形狀完全不同,混在一棒會讓研究員把兩套判斷標準搞混。

§14

🔴 開卡前更正了跨 arc 總表一處錯誤

總表 §4 的排名表把這支排在第 4 順位,理由寫「日誌轉送的目標設定裡包含帳密」——這句不成立。首腦開了建表腳本逐欄核對,那張表只有主機、埠號、通訊協定、開關這些欄位,沒有任何帳號密碼欄位。

所以這支的原始排序理由有一半是錯的。但仍值得掃,理由改成上面「一頁看完」講的那件事:風險不在「設定裡存了什麼」,而在「誰改得動、改了會把什麼導去哪裡」。

§15

已經查證過的,掃到不要重報

這三件首腦在開卡前已開檔核對,掃到請標「已知、非新發現」:

  1. 三支轉送端點在套件裡沒掛任何權限裝飾器——這是刻意設計,不是漏守門。 套件檔頭明寫「守門由宿主注入」,首腦已追到主專案 core/plugins/api_log.py:185-186 確實注入了「要登入 + 要有對應權限」,而且讀與寫分成兩個不同權限。研究員若只看套件會誤報成「無守門」,這是本 arc 最可能的誤報來源。
  2. 轉送設定表沒有做客戶隔離,是刻意的。 建表腳本檔頭寫明理由:這是平台層設定不是客戶資料;而且掛載發生在沒有網頁請求脈絡的背景執行緒,隔離機制需要的變數當下不存在,掛了只會讓背景讀取永遠是空的。存取控制靠應用層守門。
  3. 操作記錄表沒有客戶隔離、也沒有客戶欄位——開發環境實查確認(五張分區表全部沒開隔離、0 條規則、無客戶欄位)。但這已登記在跨 arc 總表第 23 項,掃到請標「與第 23 項同一件事」,不要另計。
§16

共同設定

項目 值
主 session 模型 Opus 5 (1M context),不要用 Sonnet(研究員繼承主 session 的內容上限,Sonnet 跑不完會被工具的「180 秒沒動作就判當掉」砍掉重派)
effort low(單一研究員掃全範圍+三位檢查員投票,不跑盤點、不做威脅建模)
focus attack-surface(跳過測試碼、打包產物、文件)
派工節奏 一次只派一棒,驗收完才派下一棒
§17

相關座標

  • 首腦手冊:.claude/skills/security-scan-lead/SKILL.md
  • 跨 arc 總表:docs/features/security-scan-consolidated/
  • 報告寫法範例:FR-096 H1 報告
  • 套件路徑:~/Projects/Jedicogy/module/jedi-python-package/jedi-log/
  • branch:jedi 套件 repo main、BE repo main(2026-09-15 開卡時實查)
§18

文件

以下全部由 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
§19

Notion 卡

卡片內容(決策紀錄、驗收條件)以 Notion 為準,本頁只記座標。

關係 卡號 標題 狀態
母案 CM-1810 FR-097 掃 jedi-log(系統日誌轉送與 API 操作記錄)資安掃描(兩棒 82 檔,只掃不修) —
子卡 CM-1811 L1 掃日誌轉送全鏈(39 檔,jedi 套件) Done
子卡 CM-1812 L2 掃 API 操作記錄全鏈(43 檔,jedi 套件) Done