FR-085.C2 掃描報告:log 全鏈(寫什麼、寫到哪、身分怎麼標)

FR-085.C2 掃描報告:log 全鏈(寫什麼、寫到哪、身分怎麼標)

  • 卡片CM-1648(母卡 CM-1646
  • 掃描範圍jedi-common 套件的 jedi_common/logger/,共 32 個受版控檔
  • 版本:commit 8aa6f060(jedi monorepo,branch feature/FR-075
  • 日期:2026-09-10
  • 工具產物jedi-common/CLAUDE-SECURITY-20260910-141953/(不入版控)

1. 🔴 一句話結論

使用者的登入密碼、以及每一個請求的登入憑證(JWT),目前是原文寫進伺服器 log 檔與資料庫的——而且產品自己另一條 log 路徑明明有遮罩功能,就是這一條沒用上。

工具這一輪的正式發現是 0 條(四個候選全被三位檢查員一致否決,否決理由我核對後都同意)。但底下最嚴重的那條工具完全沒碰到——它需要同時看套件與主產品兩邊才看得出來,而工具這一輪只掃套件。

一個好消息:卡片原本認為「最重要」的那條(log 上的操作者可以被偽造不成立——那段程式碼從頭到尾沒有被啟用過,是死碼。


2. 這一棒在檢查什麼(白話)

jedi-commonlogger/ 是全站 log 的總開關。它決定三件事:

  1. 每一行 log 長什麼樣、上面標的「這是誰做的」從哪來
  2. log 寫到哪裡——螢幕、檔案(log/app.log)、資料庫的 system_logs 表、以及一個遠端監控服務
  3. 哪些程式的 log 會被寫進資料庫

這一棒要回答三個問題:

  • 會不會把不該寫的東西寫出去?(密碼、憑證、完整網址、完整錯誤堆疊)
  • log 上標的「操作者」能不能被偽造?(能的話稽核紀錄就沒有意義)
  • 寫進資料庫的那份,有沒有做到「每個客戶只看得到自己的」?

只找問題、不修問題。 底下所有「怎麼修」都只是建議,沒有動任何一行程式碼。


3. 掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 嚴重度
C2-1 每個請求的完整標頭與內容原文寫進 log,包含登入密碼與登入憑證 拿得到 log 檔或資料庫的人,可直接取得使用者密碼與可冒用的登入憑證 能讀 log/app.log(主機上的檔案)或能查 system_logs common/middleware/app_mw.py:45-46(主產品) HIGH
C2-2 system_logs 表完全沒有客戶隔離,且沒有可用來隔離的欄位 任何連得到資料庫的帳號都看得到全部客戶的 log;目前該表有 66 萬筆 能用 cm_app 或更高權限連資料庫 DEV 實查:RLS 關閉、0 條規則 MEDIUM
C2-3 完整錯誤堆疊寫進上面那張沒有隔離的表 堆疊會夾帶內部路徑、SQL 片段,與 C2-1 疊加時夾帶憑證 同 C2-2 db_log/db_handler.py:15-16 MEDIUM
C2-4 寫 log 到資料庫失敗會把原本的請求一起弄壞,且能否運作取決於 handler 的排列順序 log 系統的故障變成業務 API 的故障(500) 資料庫暫時寫不進去,或有人調整了 handler 順序 db_log/db_handler.py:13-30 MEDIUM
C2-5 出貨設定漏了一個環境變數,正式環境實際套用的是開發用的 log 設定 上面每一條的影響範圍從「開發機」擴大到客戶的正式機 就是出貨預設值,不需任何條件 docker/production/guidant.env(缺 RUN_ENV MEDIUM(放大器)
C2-6 遠端監控連線在 import 時就啟動、位址寫死、不加密 沒有人接收時靜默重試;有人接收時追蹤資料明文外送 無(開機就執行) custom_formatter.py:27:35 LOW
C2-7 正式與測試環境把兩個 logger 設成最詳細等級 log 量放大,且 SQL 層 log 可能帶查詢內容 就是出貨預設值 config_prod.py:57:82 LOW
C2-8 log 上的操作者取自請求標頭、可被偽造(卡片認定「本棒最重要」) 不成立——這段程式碼從未被啟用,見第 5 節① middleware.py:13 無(死碼)

C2-1 是本棒唯一的 HIGH,也是唯一需要盡快處理的。 C2-2/C2-3/C2-5 是它的放大器:沒有隔離的表 + 正式機套用開發設定,讓 C2-1 的影響從「開發機的檔案」變成「客戶正式機的資料庫與檔案」。


4. 每條發現的詳述

C2-1(HIGH)每個請求的完整標頭與內容原文寫進 log——包含密碼與登入憑證

(工具未報,runner 人工查證)

這是什麼問題(白話)

主產品在每個請求進來時,會寫三行 log。其中兩行是這樣:

logger.info(f"Request Headers: {request.headers}")   # 第 45 行
logger.info(f"Request Body: {request.data}")         # 第 46 行
  • 第 45 行所有請求標頭原封不動印出來。使用者登入後,每個請求都會帶一個 Authorization: Bearer <一長串憑證> 標頭——那串憑證等於通行證,拿到就能冒充該使用者,不需要知道密碼。
  • 第 46 行請求內容原封不動印出來。登入請求的內容就是 {"login_name": "...", "password": "..."}——使用者的密碼,原文

真正讓這件事嚴重的是對比產品有遮罩功能,而且就在同一個檔案裡用著。

同一支 app_mw.py,往下 5 行的 :51 就是 req_data = mark_password(request.data.decode())——寫進另一張表(api_logs)之前有先遮罩。遮罩函式 mark_passwordjedi_common/utils/common_utils.py:150)會把 passwordtokensecretprivate_key 這類欄位的值換成 ***

所以同一個請求的同一份內容,在同一支程式裡走了兩條路

路徑 有沒有遮罩 寫到哪
:51api_logs mark_password 資料庫 api_logs
:45-46logger.info() 沒有 螢幕 + log/app.log 檔 + system_logs

這回答了卡片重點⑧:共用地基層遮罩工具,但 logger 這一整條鏈從頭到尾沒有任何一處呼叫它(全樹 grep mark_password 確認:只有 app_mw.py 兩處與測試檔用到,jedi_common/logger/ 底下零命中)。

出事會怎樣

拿得到 log 的人可以直接取得兩樣東西:

  1. 使用者密碼原文(來自登入請求)——可直接登入,也可拿去試其他系統(很多人重複用密碼)
  2. 登入憑證 JWT(來自每個已登入請求)——不需要密碼就能冒充該使用者,直到憑證過期

而「拿得到 log 的人」比想像中多:

  • log/app.log:正式部署會把它掛到主機的 /srv/guidant-ai/logdocker-compose.yml:147),任何能進主機的人都讀得到
  • system_logs完全沒有客戶隔離(見 C2-2),任何連得到資料庫的帳號看得到全部
  • 螢幕輸出docker logs 看得到,且會進 docker 的 json 檔

這是本專案第三次同型問題——前兩次是 CM-1606(SMTP 密碼寫進 log)與 FR-076 L3(License Center 的 log 遮蔽失效)。差別在於:前兩次是個別功能寫錯,這次是「每一個請求都會發生」。

要先有什麼才打得到

  • 不需要任何攻擊技巧就會發生——這是每個請求的預設行為,log 已經寫在那裡了
  • 要取得需要:能讀主機上的 log 檔,或能查資料庫的 system_logs
  • 不需要登入權限就能讓自己的密碼被寫進去(登入請求本身就會觸發),但要讀取才需要上述存取權

為什麼是 HIGH 而不是 CRITICAL:攻擊者需要先有主機或資料庫的讀取權才拿得到。它不是「從外網直接打進來」的洞,而是「一旦有人進到內部,或有人不當存取 log,損失立刻放大到全體使用者的密碼」。

在哪裡

  • compliance-manager-be/common/middleware/app_mw.py:45logger.info(f"Request Headers: {request.headers}")憑證外洩點
  • compliance-manager-be/common/middleware/app_mw.py:46logger.info(f"Request Body: {request.data}")密碼外洩點
  • compliance-manager-be/common/middleware/__init__.py:2logger = logging.getLogger("middleware"),確認用的是 middleware 這個 logger
  • jedi-common/jedi_common/logger/config_dev.py:84-88middleware logger 掛了 ['app', 'file', 'db'] 三個輸出:螢幕、log/app.log、資料庫
  • 對照組(有遮罩的那條)app_mw.py:51:101 — 寫 api_logs 前先 mark_password
  • 遮罩工具本身jedi-common/jedi_common/utils/common_utils.py:150-164(涵蓋 password/passphrase/secret/token/credential/authorization/api_key/private_key)
  • 接線確認core/app_factory.py:259 init_app_interceptor(app)(這支中介層確實有掛上,不是死碼)
  • log 檔掛到主機docker/production/docker-compose.yml:147

怎麼修

最小改動(建議先做這個):把 app_mw.py:45-46 兩行改成遮罩過的版本,並且不要印全部標頭

# 只印需要排錯的標頭,且不含 Authorization / Cookie
safe_headers = {k: v for k, v in request.headers.items()
                if k.lower() not in ("authorization", "cookie", "x-api-key")}
logger.info(f"Request Headers: {safe_headers}")
logger.info(f"Request Body: {mark_password(request.data.decode(errors='replace'))}")

mark_password 已經 import 在這支檔案裡(:11),不必新增相依。

根治(建議一併排程)在 logger 這一層加一道遮罩,不要只靠呼叫端記得。做法是在 CustomFormatter.format()custom_formatter.py:44)回傳前,對 record.getMessage() 的結果套一次 mark_password。這樣不管哪支程式寫了什麼進 log,都會先過一次遮罩。

理由:目前的設計是「每個呼叫端自己記得遮罩」,而這已經漏了三次(CM-1606、FR-076 L3、本條)。只要遮罩的責任還在呼叫端,就會有第四次。 放在 formatter 這一層的代價是每行 log 多跑一次正規表示式,對照風險是划算的。

注意mark_password 只認得 JSON 格式的 "key": "value"。標頭是 Authorization: Bearer xxx 的形式,不是 JSON,遮不到——所以標頭那行必須另外處理(用上面的白名單做法),不能只靠加遮罩。

首腦核對註記工具未報,runner 人工查證。 工具沒碰到是可以理解的——問題檔在主產品 repo,不在本次掃描範圍(jedi-common)內,而「這條 log 會流到哪」則要看套件的設定檔,兩邊都看得到才拼得出來。我逐項核對了:① app_mw.py:45-46 原文屬實;② logger 名稱是 middleware__init__.py:2);③ config_dev.py:84-88 確實把 middleware 掛上 db handler;④ 這支中介層確實有接線(app_factory.py:259);⑤ 同檔 :51 確實有用遮罩,形成對比;⑥ 全樹 grep 確認 jedi_common/logger/ 零處呼叫 mark_password未實測——沒有實際發一個登入請求去看 log 內容,也依卡片紀律沒有讀取任何一列 system_logs 的 message 原文。結論是讀程式碼與設定檔的推論。


C2-2(MEDIUM)system_logs 表完全沒有客戶隔離,而且沒有可以拿來隔離的欄位

(卡片重點③b 指定實查,已用 DEV 庫查證)

這是什麼問題(白話)

產品是多客戶共用一套系統的,靠資料庫的一道機制讓每個客戶只看得到自己的資料(RLS,就是「每個客戶只能看自己資料」的資料庫隔離機制)。

system_logs 這張表沒有開這道機制,一條規則都沒有。

我用 DEV 庫實際查了(唯讀,沒有改任何東西):

查什麼 結果
有沒有開隔離 沒有relrowsecurity = f
有幾條隔離規則 0 條
表裡有多少筆 668,813 筆(其中 ERROR 等級 3,646 筆)
誰能讀 cm_app(應用程式用的帳號)有完整讀寫權

更根本的問題是:這張表連「哪個客戶」的欄位都沒有。 它的欄位是 act_time / user_uid / user_name / level / method / message / event_code / path / func_name / line_no——沒有 tenant 或客戶編號。所以就算現在想開隔離也開不了,得先改表結構。

出事會怎樣

任何連得到資料庫的帳號(包括應用程式自己用的 cm_app)都看得到全部客戶的 log 紀錄。log 內容包含操作者的帳號 uid 與暱稱、他碰了哪支程式、出了什麼錯——等於一份跨全部客戶的行為紀錄

與 C2-1 疊加時最麻煩:C2-1 讓密碼與憑證寫進這張表,而這張表沒有任何隔離。

對照組:同樣是 log 表的 api_logs 也沒有開隔離(一併查證)。所以這不是「漏了這一張」,而是兩張 log 表都在隔離機制之外

為什麼是 MEDIUM 不是 HIGH:要看到這張表得先有資料庫存取權。應用程式的一般查詢路徑不會去讀 system_logs(這張表目前沒有任何正式的讀取端點,見第 5 節),所以不會經由 API 洩漏給一般使用者。它的風險在於「內部人員或已進入內部的攻擊者」,而不是外部直接可打。

要先有什麼才打得到

  • 能用 cm_app 或更高權限連上資料庫(例如已進到主機、或拿到資料庫密碼——順帶一提,資料庫密碼散落 249 個檔的問題是既有的 CM-1629)
  • 不需要任何應用層權限——這張表沒有 API 讀取端點,反過來說也沒有 API 層的守門保護它

在哪裡

  • DEV 庫實查(唯讀):SELECT relrowsecurity FROM pg_class WHERE relname='system_logs'fSELECT count(*) FROM pg_policies WHERE tablename='system_logs'0
  • 表結構:\d public.system_logs(12 個欄位,無 tenant 欄位,依 act_time 做時間分割,5 個分割)
  • 權限:cm_app=arwd/nicsmgr(完整讀寫)
  • 寫入端:jedi-common/jedi_common/logger/db_log/db_handler.py:30
  • 表定義:主產品 scripts/init/02-schema.sql

怎麼修

這條要分兩層看,建議先做第一層

  1. 先確認「這張表該不該分客戶」。系統層 log(排程、啟動、框架錯誤)本來就不屬於任何客戶,硬加隔離反而會讓維運看不到全貌。如果結論是「不分客戶、只給維運看」,那正確的修法不是加 RLS,而是把存取權收緊:撤掉 cm_app 對這張表的 SELECT 權(應用程式只需要寫,不需要讀),只留給管理帳號。這是成本最低、效果最直接的一步。
  2. 如果決定要分客戶,就得先加欄位(tenant_id)、回填既有 66 萬筆、再加 policy。這是有成本的工程,要先有第 1 點的結論再做。

無論選哪條,C2-1 都要先修——把密碼與憑證擋在 log 之外,比事後討論誰能讀這張表更有效。

首腦核對註記卡片重點③b 明確要求實查,已完成。 查詢全部唯讀(pg_classpg_policies\dcount(*)/依 level 分組計數),依卡片紀律沒有讀取任何一列的 message 原文——那正是可能夾帶憑證的欄位。只查了 DEV 庫;STG/POC 未查(依環境異動鐵律,唯讀本可做,但本棒沒有必要——schema 來自同一份 init 腳本,且真正該修的是 C2-1)。


C2-3(MEDIUM)完整錯誤堆疊寫進那張沒有隔離的表

這是什麼問題(白話)

程式出錯時,會把完整的錯誤堆疊(就是那一長串「哪個檔案第幾行呼叫了哪個函式」的清單)接在 log 訊息後面,一起寫進資料庫:

if record.exc_text:
    message += '\n\n' + record.exc_text

錯誤堆疊本身會揭露內部檔案路徑、函式名稱、以及有時候的 SQL 片段。單獨看不算嚴重——這些資訊沒有直接的攻擊價值,而且是寫進內部資料庫、不是回給前端(回給前端的那條是 C1 已經報過的 sql_exception.py)。

它之所以值得列出來,是因為疊加效應:這些堆疊進的是一張完全沒有客戶隔離的表(C2-2),而且當請求本身夾帶密碼時(C2-1),堆疊裡也可能一併帶出那些值。DEV 庫目前有 3,646 筆 ERROR 等級的紀錄。

出事會怎樣

拿得到 system_logs 的人可以拼出系統的內部結構(哪些模組、哪些函式、哪裡容易出錯),這對後續攻擊有幫助但不直接造成損失。真正的風險來自與 C2-1 的組合。

要先有什麼才打得到

  • 同 C2-2(能查資料庫)
  • 要看到有價值的內容,還需要系統真的出過帶敏感值的錯

在哪裡

  • jedi-common/jedi_common/logger/db_log/db_handler.py:15-16if record.exc_text: message += '\n\n' + record.exc_text
  • 落點:system_logs.messagetext 型別,無長度上限

怎麼修

三個選項,建議第二個:

  1. 不寫堆疊進資料庫(只留在檔案)——但這會讓維運查問題變困難,不建議
  2. 寫之前先過一次遮罩——與 C2-1 的根治修法是同一件事(在 formatter 層加遮罩,堆疊也會被遮到)。一次改動同時解決兩條
  3. 限制堆疊長度(例如只留最後 20 行)——治標,減少夾帶機率但不根治

首腦核對註記:工具的候選 C3 碰到了這個檔案,但問的是另一件事(handler 會不會炸,見 C2-4),沒有問「寫了什麼進去」。本條為 runner 人工查證。未讀取任何實際 log 內容,判斷來自程式碼結構與 ERROR 筆數統計。


C2-4(MEDIUM)寫 log 到資料庫失敗會把原本的請求一起弄壞,而且它能運作是靠運氣

(工具候選 C3,三票一致否決;我同意「不是資安漏洞」,但認為是該修的穩定性問題)

這是什麼問題(白話)

負責把 log 寫進資料庫的那段程式(db_handler.pyemit)有兩個問題:

問題一:沒有防護,寫失敗會往外炸。

一般來說,log 系統壞掉不應該影響業務——log 寫不進去,頂多少一筆紀錄,請求還是要正常完成。但這段程式沒有任何錯誤攔截(沒有 try/except),所以資料庫一旦寫不進去,錯誤會沿著呼叫鏈往回傳,把原本那個請求打成 500

問題二:它能運作,靠的是 handler 剛好的排列順序。

這段程式讀了兩個欄位:record.asctime(時間)與 record.user_uid(操作者)。這兩個欄位不是本來就有的——要等某個 handler「格式化」過這筆紀錄之後才會被補上去。

目前設定裡,資料庫 handler 排在第三個(['app', 'file', 'db']),前兩個跑的時候順手把這兩個欄位補上了,所以第三個才讀得到。如果有人把順序調換、或拿掉前面兩個,這段程式就會當場出錯——而依問題一,那個錯誤會把請求一起打死。

出事會怎樣

  • 資料庫短暫寫不進去(連線滿了、磁碟滿了、分割表沒有對應區間)→ 正在處理的請求跟著失敗,而且錯誤訊息會指向 log 系統,不是真正的問題所在,很難查
  • 有人調整 log 設定的 handler 順序 → 每一個會寫 log 的請求都壞掉

這不是資安漏洞,三位檢查員一致否決的理由是正確的:找不到攻擊者可以控制的觸發點。會寫進去的欄位裡,有長度上限的(user_name 500 字、method 500 字)都是系統自己產生的,唯一受外部影響的 message 是無上限的 text 欄位。所以攻擊者沒辦法故意送一個超長的值把它撐爆。

我把它列出來,是因為它是「會在最不方便的時候壞掉」的那種問題——資料庫出狀況的當下,正是最需要 log 的時候,而這個設計會讓 log 系統反過來擴大故障。

要先有什麼才打得到

  • 不需要攻擊者——資料庫暫時寫不進去就會發生
  • 或有人修改 log 設定時調動了 handler 順序

在哪裡

  • jedi-common/jedi_common/logger/db_log/db_handler.py:13-30emit() 全段,沒有 try/except、沒有 handleError
  • db_handler.py:19datetime.strptime(record.asctime, ...)asctime 是格式化後才有的欄位
  • db_handler.py:20-21record.user_uidrecord.user_nickname,由 CustomFormatter.format()custom_formatter.py:46-49)補上
  • config_dev.py:55 等 — handler 順序 ['app', 'file', 'db']db 必須排最後才能運作
  • config_dev.py:18-20db 這個 formatter 沒有給格式字串,所以它自己不會補上 asctime

怎麼修

兩處都要改,各一行等級的改動:

  1. 加防護emit() 整段包 try/except Exception: self.handleError(record)handleError 是 Python 標準庫給 log handler 用的標準做法——把錯誤印到 stderr 就好,不往外傳。log 系統的故障不該變成業務的故障。
  2. 不要依賴順序:在 emit() 開頭先呼叫 self.format(record),自己把需要的欄位補齊,而不是期待別人先跑過。或者退一步,用 record.created(這個欄位本來就有,是時間戳)取代 record.asctime,就不需要別人先格式化。

首腦核對註記:工具候選 C3,三位檢查員一致否決(0:3),理由是「缺攻擊者可控的觸發點」。我核對後同意這個否決——確實不是資安漏洞,我沒有把它算進資安發現,而是列為穩定性問題。三位檢查員的分析品質很高:其中一位甚至查到 Python 標準庫 logging/__init__.pyhandle() 只有 try/finally 不吞例外,證實錯誤真的會往外傳;另一位查到 guidant.env 沒設 RUN_ENV 所以正式機也掛著這個 handler(這一點我獨立驗證了,見 C2-5)。


C2-5(MEDIUM,放大器)出貨設定漏了一個環境變數,正式環境實際套用開發用的 log 設定

這是什麼問題(白話)

log 有三套設定:開發用(dev)、測試用(stg)、正式用(prod)。程式靠一個叫 RUN_ENV 的環境變數決定用哪一套:

RUN_ENV = os.getenv("RUN_ENV", "dev")   # 沒設就用 dev

出貨用的設定檔 docker/production/guidant.env 裡沒有 RUN_ENV 這一行。 它只有 ENV=PRD(那是另一個變數,管資料庫連線設定,不管 log)。

所以客戶正式機跑的是開發用的 log 設定

這為什麼要緊:三套設定的差異不小。

開發設定(實際生效的) 正式設定(原本該生效的)
寫進資料庫 7 個 logger 都寫 ❌ 完全不寫
寫進檔案 log/app.log ✅ 寫 ❌ 不寫(被註解掉)
middleware logger (就是 C2-1 那條) 沒有定義

也就是說:C2-1 那條把密碼寫進資料庫與檔案的路徑,在正式設定裡本來是不存在的——正式設定根本沒有定義 middleware 這個 logger,也沒有掛資料庫 handler。是這個漏掉的環境變數,讓開發環境的行為原封不動搬到了客戶的正式機上。

出事會怎樣

它自己不造成損失,但它把 C2-1、C2-2、C2-3 的影響範圍從「開發機」擴大到「客戶的正式機」。沒有這條,C2-1 就只是開發環境的問題。

順帶一提,這也解釋了為什麼正式機會有 log/app.log(且掛到主機 /srv/guidant-ai/log)——按正式設定它根本不該存在。

要先有什麼才打得到

  • 不需要任何條件,這是出貨的預設狀態

在哪裡

  • jedi-common/jedi_common/logger/config_logger.py:44RUN_ENV = os.getenv("RUN_ENV", "dev")(沒設就當 dev)
  • compliance-manager-be/docker/production/guidant.env全檔 24 行,沒有 RUN_ENV,只有 :15ENV=PRD
  • 對照:compliance-manager-be/.env.sample:63 — 開發用的範本 RUN_ENV=dev
  • 差異證據:config_dev.py:43-47(有 db handler)vs config_prod.py:18-30沒有 db handler,檔案 handler 也被註解掉 :31-38
  • config_prod.py:44-84 — logger 清單裡沒有 middleware

怎麼修

先確認要哪一種行為,再改——這條不能直接補上 RUN_ENV=prod 就了事:

  • 如果正式機把 log 寫進資料庫(多數產品會要,方便維運查問題),那就config_prod.py,把該掛的 handler 補上,並且先修 C2-1(否則等於正式承認要把密碼寫進資料庫)
  • 如果不要,那就在 guidant.envRUN_ENV=prod——但要先確認維運不依賴 log/app.log(目前 compose 有掛載它,代表有人在用)

無論選哪個,都建議把「沒設 RUN_ENV 就當 dev」這個預設改掉——正式環境因為少一行設定就套用開發設定,是很容易重演的錯誤。建議改成沒設就當 prod(安全的那一邊),或啟動時明確報錯要求設定。

首腦核對註記:工具的檢查員在否決 C3 時順帶提到了這一點,我獨立驗證過:實際打開 guidant.env 確認 24 行內無 RUN_ENV(只有 ENV=PRD),並逐一比對三個設定檔的 handler 與 logger 清單。這條是本報告裡「工具間接提到、我核實後認為值得單獨列出」的一條——因為它是其他三條的放大器,不列出來會讓人低估影響範圍。


C2-6(LOW)遠端監控連線在程式載入時就啟動,位址寫死且不加密

這是什麼問題(白話)

有一段程式在被載入的當下(不是「被呼叫的時候」,是 import 進來就執行)建立了兩條到遠端監控服務的連線:

processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="localhost:4317", insecure=True))
reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint="localhost:4317", insecure=True))

OTLP(就是把程式的追蹤資料送到遠端監控服務的標準協定)的位址寫死成本機的 4317 埠,而且 insecure=True 意思是不加密、不驗證對方身分

我查了:整個主產品沒有任何地方部署 4317 這個服務(grep 全樹的程式碼、compose、Dockerfile、環境設定檔,零命中)。

出事會怎樣

分兩種情況:

  • 目前的情況(沒人在 4317 聽):資料送不出去。OTLP 的匯出器會累積在記憶體佇列裡、定期重試、失敗後丟棄。影響是持續的無效重試與少量記憶體佔用,不是資安問題。這也是它是 LOW 的主因。
  • 如果哪天有人在同一台機器上開了 4317(例如另一個容器、或攻擊者已進到主機):追蹤資料會明文送過去。追蹤資料含函式呼叫路徑與時間,敏感度低於 log 本身,但仍是不該外流的內部資訊。

另一個角度的問題(不是資安,是設計):這種「import 就產生副作用」的寫法本身很脆弱——同一支套件的 config_logger.py 檔頭(:1-34)花了整整 34 行說明「logging 設定必須是宿主明確呼叫,不能是 import 副作用」,還記錄了因此踩過的坑(CM-1513)。同一個套件裡,一邊立了規矩,另一邊還留著違反規矩的程式碼。

要先有什麼才打得到

  • 目前狀態下無法利用(沒有接收端)
  • 要有實質影響,需要有人在該機器的 4317 埠開一個接收服務

在哪裡

  • jedi-common/jedi_common/logger/custom_formatter.py:27 — span 匯出器,endpoint="localhost:4317", insecure=True
  • jedi-common/jedi_common/logger/custom_formatter.py:35 — metric 匯出器,同樣寫死
  • custom_formatter.py:22-40整段在模組頂層,import 就執行
  • custom_formatter.py:40LoggingInstrumentor().instrument(),同樣是 import 副作用
  • 對照(同套件的正確做法):config_logger.py:1-34 的檔頭說明

怎麼修

  1. 位址改成可設定(環境變數),並且預設不啟用——沒設就不建立匯出器。這樣沒有監控服務的部署就不會有無效重試
  2. 把這段從模組頂層搬進一個明確的初始化函式,比照 configure_logging() 的做法,由宿主決定要不要呼叫
  3. 若日後真的要用,insecure=True 要改成走加密連線

⚠️ 新增環境變數前記得先 grep 既有變數(CLAUDE.md 鐵則,FR-064 曾因此自創了與既有變數重複的名字)。

首腦核對註記:工具未報(工具未報,runner 人工查證)。卡片重點④指定要追。我查證了「有沒有人在 4317 聽」——全樹 grep 4317otlpOTLP 零命中,所以判定為目前無接收端。未實測——沒有實際觀察匯出器在無接收端時的記憶體行為,那需要跑起來量測。


C2-7(LOW)正式與測試環境把兩個 logger 設成最詳細等級

這是什麼問題(白話)

log 有等級之分,DEBUG 是最詳細的一級,一般只在開發時用。正式環境的設定裡有兩個 logger 被設成 DEBUG

  • infra(資料存取層)
  • sqlalchemy.orm(資料庫套件本身)

出事會怎樣

  • log 量放大DEBUG 等級會產生大量紀錄。docker 那層有上限保護(每容器 20MB×5,docker-compose.yml:108),所以不會撐爆磁碟,但會讓有用的訊息被淹沒
  • sqlalchemy.orm 的 DEBUG 可能帶查詢內容:這個套件在 DEBUG 等級會輸出 SQL 相關的訊息。需要說明的是:真正會印出完整 SQL 與參數值的是 sqlalchemy.engine 這個 logger(設定裡沒有動到它),sqlalchemy.orm 印的多是物件關聯的內部運作。所以這條的實際洩漏風險比乍看小,我判 LOW 而不是 MEDIUM

為什麼還是列出來infra 是我們自己的資料存取層,把它設成 DEBUG 意味著開發時寫的除錯訊息會全部進正式環境的 log——而那些訊息的內容沒有人審過。

要先有什麼才打得到

  • 就是出貨預設值
  • (但因 C2-5,正式機實際套用的是 dev 設定,那份設定裡 infraINFO——所以這條目前反而沒生效

在哪裡

  • jedi-common/jedi_common/logger/config_prod.py:55-59infralevel: DEBUG
  • jedi-common/jedi_common/logger/config_prod.py:80-84sqlalchemy.ormlevel: DEBUG
  • config_stg.py:55-59:80-84 — 測試環境同樣設定
  • 對照:config_dev.py:64-68infraINFO:99-103sqlalchemy.ormERROR開發設定反而比正式設定保守

怎麼修

config_prod.pyconfig_stg.py 這兩處的 DEBUG 改成 INFOsqlalchemy.orm 建議直接 WARNING,比照開發設定的 ERROR 也可以)。

這條與 C2-5 要一起看:修 C2-5(讓正式機真的套用正式設定)之前,得先修這條,否則正式設定一生效,log 量會突然暴增。

首腦核對註記:工具未報(工具未報,runner 人工查證)。卡片重點⑦指定要逐項對照三個設定檔,已完成(見第 5 節⑦的完整對照表)。我特意把「sqlalchemy.orm 不等於 sqlalchemy.engine」寫清楚——如果不區分,這條會被誇大成「SQL 全量進 log」,那不是事實。


C2-8(無問題/死碼)log 上的操作者取自請求標頭——查了,這段程式從未被啟用

(卡片認定「本棒最重要」;工具候選 C1,三票一致否決;我核對後同意否決)

這是什麼問題(白話)

卡片指出:log 上標的「這是誰做的」,是從請求標頭 X-UserId 直接讀來的:

user_id_var.set(request.headers.get("X-UserId", "System"))

標頭是呼叫端自己送的,任何人都能隨便填。如果稽核紀錄靠它標示操作者,那任何人送一個 X-UserId: admin 就能讓紀錄寫成別人做的。卡片把這條列為本棒最重要的疑點。

查證結論:不成立,因為這段程式碼從來沒有被啟用過。

那行程式包在一個叫 logger_middleware(app) 的函式裡。這種函式要「被呼叫」才會把行為掛到網站上。我 grep 了整個 jedi monorepo(25 支套件)與主產品,找不到任何一處呼叫它——只有定義本身。

同樣的情況也出現在 decorator.py:11jedi_logger(另一支也讀 X-UserId 的裝飾器):同樣零呼叫者

所以現況是user_id_var 這個變數從來沒有被設定過,永遠是預設值 "System"

那 log 上的操作者是哪來的? 是另一套機制——CustomFormatter.format()custom_formatter.py:45-49)呼叫 get_user_context(),從登入憑證(JWT)取得身分。那是後端自己驗證過的,不可偽造

但這件事留下了一個真的問題(不是資安問題):因為 user_id_var 永遠是 "System",而螢幕與檔案的 log 格式字串用的正是它([%(userid)s]),所以:

log/app.log 與螢幕輸出上的每一行 log,操作者欄位永遠顯示 System

真正的身分(user_login_nameuser_uiduser_nickname)雖然有被算出來,卻沒有被放進螢幕與檔案的格式字串裡——只有寫進資料庫的那份用得到。所以出事要查「誰做的」,只能查資料庫那份,看檔案是查不出來的。 這是可維護性問題,值得記一筆。

在哪裡

  • jedi-common/jedi_common/logger/middleware.py:9def logger_middleware(app):零呼叫者
  • jedi-common/jedi_common/logger/middleware.py:13 — 讀 X-UserId 的那行
  • jedi-common/jedi_common/logger/decorator.py:7def jedi_logger(logger):零呼叫者
  • jedi-common/jedi_common/logger/decorator.py:11 — 同樣讀 X-UserId
  • custom_formatter.py:19user_id_var 定義,default="System"
  • custom_formatter.py:45-49真正在用的身分來源get_user_context(),來自 JWT)
  • config_dev.py:12 — 格式字串 [%(userid)s],用的是死掉的那個變數
  • 全樹 grep 證據logger_middlewarejedi_loggeruser_id_var 在 jedi monorepo + 主產品的所有 .py 檔(排除 .venv 與 site-packages)中,除定義處外零命中

怎麼修

  1. 刪掉 middleware.pydecorator.py 這兩支死碼。留著的風險是:哪天有人看到「這裡有現成的中介層」就掛上去,那個可偽造的身分就活了。這是真實的風險——它看起來完全像是能用的東西
  2. 順手把真實身分放進螢幕與檔案的格式字串config_dev.py:12[%(userid)s] 改成 [%(user_login_name)s](那個欄位已經被算出來了,只是沒被用)。這樣檔案 log 才查得出誰做的

首腦核對註記工具候選 C1,三位檢查員一致否決(0:3),三位都以「這是死碼」為理由,並各自獨立做了全樹搜尋。我重新獨立 grep 驗證過,確認結論正確。 這是本輪工具表現最好的一條——卡片(含首腦盤點)認定為「本棒最重要」的疑點,實際上是死碼,工具三票一致把它擋了下來。如果沒有這道查證,這條會被當成 HIGH 寫進報告,然後有人花時間去修一段根本沒在跑的程式。這正是三人面板存在的價值。


5. 卡片「重點看什麼」八項逐項回覆

卡片(CM-1648)列了八項要追的點。逐項交代,沒有留白

# 卡片的疑問 結論
log 上的操作者直接取自 request header X-UserIdmiddleware.py:13decorator.py:11)。這兩個 contextvar 最後流到哪?跟 auth_context.py 那個真正的登入身分是不是兩套?哪一套進了 system_logs?(卡片標為「本棒最重要」) 不成立——兩支都是死碼,見 C2-8。 全樹 grep 確認 logger_middlewarejedi_logger 零呼叫者,所以 user_id_var 永遠是預設值 "System"

確實是兩套身分並存,但可偽造的那套沒有生效:
可偽造的(死的)X-UserIduser_id_varrecord.userid只出現在螢幕與檔案的格式字串 [%(userid)s]
不可偽造的(活的):JWT → get_user_context()record.user_uiduser_nicknamesystem_logs

所以進資料庫的是可信的那套。 完整流向見下方「身分流向表」。

但留下一個真問題
:檔案與螢幕 log 的操作者欄位永遠顯示 System,真實身分算出來了卻沒放進格式字串——出事查「誰做的」只能查資料庫,查檔案查不到。
完整 URL 含 query string 進 logdecorator.py:13)。哪些端點用了這個 decorator?有沒有 token/密碼走 query string 的端點? 這個 decorator 是死碼(零使用者),所以卡片問的那條路不存在。

但我在追這一項時找到了更嚴重的東西——主產品有另一支確實在跑的中介層在做同樣性質的事,而且更糟:它印的不是 URL,是完整標頭與完整請求內容,也就是 C2-1(本棒唯一的 HIGH)

app_mw.py:45-46 每個請求都印 request.headers(含 Authorization: Bearer <JWT>)與 request.data(含登入密碼),完全沒有遮罩,而同一支檔案 :51 寫另一張表時有遮罩

卡片問的方向是對的,只是問錯了檔案。
完整 traceback 落資料庫db_handler.py:14-16)。(a) 會不會夾帶區域變數值?(b) system_logs 有沒有 RLS——請用 DEV 庫實查;沒有的話誰能讀? (a) 不會夾帶區域變數值,但會夾帶內部路徑與函式名,見 C2-3。Python 標準的 exc_text 只含呼叫堆疊與例外訊息,不含區域變數的值(那需要 traceback.format_exc 搭配特殊設定或第三方套件才會有)。所以「密碼從堆疊漏出去」這條路不成立——密碼是從 C2-1 那條路漏的。

(b) 實查完成,答案是完全沒有隔離,見 C2-2
relrowsecurity = f(沒開)
pg_policies 0 條規則
表裡沒有 tenant 欄位(12 個欄位全查過),所以現在想開也開不了
• 目前 668,813 筆,其中 ERROR 3,646 筆
• 誰能讀:cm_app(應用程式帳號)有完整讀寫權
對照:api_logs 同樣沒有 RLS——不是漏了一張,是兩張 log 表都在隔離之外

依卡片紀律,只查欄位與筆數,未讀取任何一列 message 原文。
OTLP 遠端追蹤 import 時就連線且寫死明文custom_formatter.py:27:35)。正式環境有沒有 4317 在聽?沒有的話 exporter 會怎樣?有的話 span 含什麼? 見 C2-6(LOW)。全樹 grep 4317otlpOTLP(含 compose、Dockerfile、env 檔)零命中——沒有任何地方部署接收端。

沒人聽的後果:OTLP 匯出器把資料放進記憶體佇列、定期嘗試送出、失敗後丟棄。是持續的無效重試與少量記憶體佔用,不會阻塞請求BatchSpanProcessor 是背景執行緒、有佇列上限)。未實測,這是依 OTLP SDK 的既定行為推論。

有人聽的話:span 內容是函式呼叫路徑與耗時,不含 SQL 參數或 request body(那要另外埋才會有)。敏感度低於 log 本身。

額外指出:22-40 整段是 import 副作用,而同一個套件的 config_logger.py 檔頭花了 34 行說明「不可用 import 副作用」(CM-1513 的教訓)。同套件裡一邊立規矩一邊違反。
formatter 把登入身分印進每一行custom_formatter.py:44-49)。這跟①的 X-UserId 是兩套身分並存嗎?哪套可信? 是兩套並存,而且可信的那套(JWT)是唯一活著的。

CustomFormatter.format() 每次格式化都呼叫 get_user_context() 取 JWT 身分,塞四個欄位進紀錄:record.userid(來自死掉的 contextvar,永遠 System)、record.user_login_namerecord.user_uidrecord.user_nickname
三個都來自 JWT,可信
)。

哪套可信:JWT 那套。 它由後端驗章後取得(auth_context),前端偽造不了。

但要指出兩件事

格式字串只用了不可信(且已死)的那個——config_dev.py:12[%(userid)s],導致檔案 log 的操作者永遠是 System
這個 formatter 每格式化一行就查一次身分,屬效能上的小浪費(非資安問題)。
log handler 自己會不會炸db_handler.py:19strptime(record.asctime)emit() 無 try/except)。炸了會怎樣?有沒有「寫 log 失敗 → 例外 → 又要寫 log → 無限遞迴」? 會炸,而且炸了會把請求一起打死,見 C2-4(MEDIUM,非資安)。

卡片的兩個懷疑都成立
emit() 確實沒有 try/except、也沒呼叫 handleError,而 Python 標準庫的 handle() 只有 try/finally 不吞例外 → 錯誤真的會竄回業務程式
record.asctime 確實是格式化後才有的欄位,而 db 這個 formatter 沒給格式字串、自己不會補 → 它能運作純粹因為 handler 排在第三個['app','file','db']),前兩個順手補上了

無限遞迴的組合:不存在。 我特別查了——寫 log 失敗時例外是往呼叫端傳(業務程式),不是在 handler 內再寫一次 log,所以不會自我遞迴。

三位檢查員一致否決為「非資安漏洞」,我同意:找不到攻擊者可控的觸發點(有長度上限的欄位都是系統自產,唯一受外部影響的 message 是無上限 text)。我把它列為穩定性問題,不計入資安發現。
三環境設定差異:DBLogHandler 掛在哪些 logger、prod 哪些是 DEBUG、log/ 檔案權限、log 檔會不會進出貨 image。逐項對照三個檔。 逐項對照完成(表在下方),並查出一個卡片沒預期到的問題:出貨настройки漏了 RUN_ENV,正式機實際套用的是開發設定——見 C2-5。

① DBLogHandler 掛在哪:dev 7 個 logger(api/app/infra/domain/common/middleware/error_handler,全 propagate=False);stg 與 prod 完全沒掛(兩個設定檔連 import 都沒有)。
② prod 哪些是 DEBUGinfra:57)與 sqlalchemy.orm:82),stg 相同 → C2-7(LOW)。註:真正會印完整 SQL 的是 sqlalchemy.engine設定裡沒動到它,故風險比乍看小。
log/ 檔案權限config_logger.py:75os.makedirs("log", exist_ok=True)沒有指定權限,取決於行程的 umask(容器內通常 0755)。RotatingFileHandler 只在 dev 設定有(config_dev.py:35-42,1MB×5),stg/prod 都註解掉了
④ log 檔會不會進 image不會——Dockerfile:131 註解明確寫「不含 log/」。但 compose 把主機目錄掛進去docker-compose.yml:147/srv/guidant-ai/log:/app/log),所以 log 會落在主機上、重啟不遺失。

🔴 但以上 stg/prod 的設定目前都沒生效,因為 guidant.env 沒有 RUN_ENV(全檔 24 行,只有 ENV=PRD),程式退回預設值 dev客戶正式機跑的是上表 dev 那一欄。
遮罩層在哪。 全站唯一的密碼遮罩是 common_utils.py:150 mark_password。logger 這一鏈有沒有呼叫它?沒有的話,request body 裡的密碼是不是原文進 log? 🔴 這一項的答案就是本棒的 HIGH(C2-1):logger 鏈零遮罩,而 request body 裡的密碼確實原文進 log。

逐項回答
logger 鏈有沒有呼叫 mark_password完全沒有。 全樹 grep 確認 jedi_common/logger/ 底下零命中
誰在用它 → 只有主產品的 app_mw.py:51(寫 api_logs 前遮 request)與 :101(遮 response),加上兩支測試檔。
密碼是不是原文進 log是。 同一支 app_mw.py:46request.data 原文丟給 logger.info(),而那個 logger(middleware)在實際生效的 dev 設定裡掛著螢幕+檔案+資料庫三個輸出。登入密碼因此原文寫進 log/app.logsystem_logs
還不只密碼:45 印完整標頭,含 Authorization: Bearer <JWT>那是可直接冒用的登入憑證

遮罩工具的能力邊界(修的時候要知道):mark_password 用正規表示式比對 JSON 形式"key": "value",涵蓋 password/passphrase/secret/token/credential/authorization/api_key/private_key。標頭不是 JSON 格式,它遮不到——所以標頭那行必須另外用白名單處理,不能只加遮罩。

🔴 身分流向表(卡片指定要畫)

三個身分來源,各自流到哪個輸出:

身分來源 可不可信 存在哪 螢幕 (stdout) 檔案 log/app.log 資料庫 system_logs OTLP 遠端
X-UserId 請求標頭 可偽造(任何人自己填) user_id_varrecord.userid ⚠️ 格式字串用它[%(userid)s] ⚠️ 格式字串用它 ❌ 沒用到 ❌ 沒用到
JWT 登入憑證 可信(後端驗過章) get_user_context()record.user_login_nameuser_uiduser_nickname 沒放進格式字串 沒放進格式字串 user_uiduser_name 進表 ❌ 沒用到
record.user_uid ✅ 可信(就是上面那套) 同上 ✅ 寫入 system_logs.user_uid

怎麼讀這張表(三個結論)

  1. 可偽造的那套(X-UserId)雖然佔著螢幕與檔案的格式位置,但它是死的——設定它的程式從未被啟用,所以那個欄位永遠印 System,沒有偽造風險,也沒有任何資訊價值。
  2. 可信的那套(JWT)只進了資料庫,沒放進螢幕與檔案的格式字串。
  3. 合起來的後果唯一有真實操作者身分的輸出是資料庫那份,而那張表沒有客戶隔離(C2-2)。查稽核只能查它,而它誰都看得到。

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

分兩層講,這兩件事的可信度差很多。

第一層:「這些問題真的存在嗎?」——高,但沒有一條做過實際攻擊驗證

八條結論全部由我逐檔開啟核對,每一條都能指到具體檔名與行號。其中三條有程式碼以外的證據

  • C2-2 是 DEV 資料庫的實際查詢結果(不是讀 schema 檔推論的)——RLS 關閉、0 條規則、668,813 筆、無 tenant 欄位
  • C2-5 是實際打開 guidant.env 數過的(24 行內無 RUN_ENV
  • C2-8 的死碼判定做了兩次獨立全樹 grep(工具三位檢查員各做一次,我再做一次)

但沒有任何一條做過實際攻擊驗證。 依卡片紀律,這一棒只讀不寫:沒有對任何環境發過請求、沒有改過任何資料、資料庫只做 SELECT 與結構查詢,沒有讀取任何一列 system_logs 的 message 原文(那正是可能夾帶憑證的欄位)。

C2-1 特別建議實測確認:發一個登入請求,然後查 log/app.log 看密碼是否真的原文出現。這是一分鐘就能做完、且能一槌定音的驗證,我沒有做是因為那需要對環境發請求(超出「只讀」紀律)。建議驗收時由決策者或下一棒補這個實測。

第二層:「只有這些嗎?」——低,這一輪遠不能算掃透

四個理由,前兩個是工具自己講的

  1. 🔴 工具本輪正式發現為 0 條,四個候選全被三票一致否決。本報告的八條全部是我人工查證的結果,工具的貢獻是「幫我擋掉一條假警報(C2-8)」與「間接提示了 C2-5」。這是連續第五個 arc 出現「工具產出遠低於人工」的情形。
  2. 🔴 工具沒有交「逐檔閱讀帳本」coverage.research: null)——所以無法證明 32 個檔每一支都被讀到結論。「只有這些」這句話沒有證據支撐。
  3. low 是快篩不是徹查:沒有清點階段、沒有威脅建模、沒有廣掃,就是「兩個研究員讀一輪 → 三個檢查員投一輪票」。
  4. 範圍之外完全沒看jedi_common/ 的其餘部分(interfaces/utils/enums/constants,那是 C3 的事)、以及 log 轉發鏈(jedi-log-forwarding 套件、主產品 api/log_forwarding/一個字都沒讀

🔴 越界說明(必讀)

本報告最重要的一條發現(C2-1)在掃描範圍之外——它的問題檔 app_mw.py 在主產品,不在 jedi-common

這是必要的追脈絡:卡片重點⑧問「logger 鏈有沒有遮罩」,要回答就得知道「誰在往這條鏈寫東西」,而寫的人在主產品。不追出去,這一項只能答「套件裡沒有遮罩」,答不出「所以密碼真的原文進了 log」。C2-5(guidant.env)與 C2-2(DEV 資料庫)同理。

但必須講清楚:主產品沒有被稽核。 我讀 app_mw.py 是為了回答「jedi-common 的 log 鏈會不會出事」,不是為了檢查主產品有沒有自己的問題。主產品的完整檢查是別的 arc 的事。

這裡乾淨不代表主產品乾淨。


7. 執行概況

項目 數值
掃描範圍 jedi_common/logger/32 個受版控檔(與卡片指定一致,全為 .py
版本 commit 8aa6f0609c4434c0fa5206d5e99e31cad249646e(branch feature/FR-075
工具 Claude Code claude-security plugin v0.11.0
effort / focus / mode low / attack-surface / codebase-scan
研究員 派 2 個,回 2 個(零失敗、零中斷)
候選發現 提出 4 條,去重後仍 4 條
檢查員投票 三個檢查員對 4 條候選各投一票,12 票全數投出,沒有漏投、沒有一條候選沒人看
投票結果 4 條全部被三票一致否決(0:3)
被否決的 4 條 C1 X-UserId 可偽造(死碼)/C2 系統 log 查詢無白名單(無呼叫者)/C3 handler 無 try/except(無可控觸發點)/C4 分頁參數被丟棄(無呼叫者)
工具正式發現 0 條
驗證章(stamp) verification.status: **verified**(完整跑完,未中斷、未撞額度、未遺失候選)
驗證輪次 1 輪
逐檔閱讀帳本 coverage.research: null)——無法證明 32 檔全讀完
執行時間 2,512 秒(約 42 分鐘
工具產物 jedi-common/CLAUDE-SECURITY-20260910-141953/(含自身 .gitignore,不入版控)
本報告發現 7 條(1 HIGH/4 MEDIUM/2 LOW)+ 1 條查證為死碼無問題。全部為 runner 人工查證;工具的貢獻是擋掉 C2-8 這條假警報

8. 建議的下一步

按投資報酬率排序:

  1. 🔴 先修 C2-1(密碼與憑證原文進 log)——這是唯一的 HIGH,而且最小改動只要動兩行app_mw.py:45-46),遮罩函式已經 import 在同一支檔案裡。建議先做一分鐘實測確認(發一個登入請求,grep log/app.log),確認後直接修。
  2. 接著決定 C2-5(RUN_ENV 漏設)怎麼處理——它是 C2-1/C2-2/C2-3 的放大器。但順序不能反:先修 C2-1 再處理它,否則等於正式承認「要把密碼寫進正式機資料庫」。另建議把「沒設就當 dev」的預設改成安全的一邊。
  3. C2-4(handler 無防護)——兩處小改動(包 try/except、不依賴 handler 順序)。非資安但會在最不方便的時候壞掉。
  4. 決定 system_logs 該不該分客戶(C2-2)——先回答這個問題再動工。若結論是「不分客戶」,最省力的修法是撤掉 cm_app 的 SELECT 權(它只需要寫);若要分客戶,得先加欄位再回填 66 萬筆,是有成本的工程。
  5. 刪掉 middleware.pydecorator.py 兩支死碼(C2-8),順手把真實身分放進檔案 log 的格式字串(config_dev.py:12)——目前檔案 log 查不出誰做的
  6. C2-6(OTLP)與 C2-7(DEBUG 等級)——都是小改動,可併入下次整理。
  7. 考慮在 formatter 層加一道統一遮罩——這是根治方向。目前「每個呼叫端自己記得遮罩」的設計已經漏了三次(CM-1606、FR-076 L3、本條 C2-1),只要責任還在呼叫端,就會有第四次

本報告依 security-scan-lead skill 第十節白話規則撰寫。所有發現均為讀程式碼、設定檔與資料庫結構的結果,未在任何環境實際執行攻擊、未對任何環境發送請求、未修改任何資料、未印出任何金鑰或密碼值。資料庫查詢全部唯讀且只對 DEV 庫,未讀取任何一列 system_logs 的 message 原文。工具產物目錄不入版控。