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

FR-078 通知投遞(jedi-notification)資安掃描

狀態:✅ 兩棒都掃完並驗收(2026-09-08)。N1 兩條(MEDIUM×2)/N2 六條(HIGH×2 MEDIUM×3 LOW×1,其中四條範圍外);四張修正卡已開,全部 Not started 文件 2 份

🔴 一頁看完

兩棒掃完,總共 8 條發現。 真正需要 PM 排程的是 4 張修正卡,其中 CM-1605 是唯一的 HIGH,其餘三張屬衛生類、優先序低。

§1

最需要知道的三件事

  1. 最嚴重的一條是「寄測試信」按鈕(CM-1605,HIGH)。使用者不改密碼欄就按測試,系統會拿平台真正的郵件帳密去登入使用者自己填的伺服器位址。而「改 SMTP 設定」這個權限是刻意下放給業務租戶管理員的——所以場景是子租戶管理員拿走平台營運方的郵件憑證,之後能用貴公司名義寄信且通過 SPF 驗證,收件人看不出是偽造。
  2. 腳本裡有兩組明文密碼,但決策者 2026-09-08 裁定不需更換(CM-1608):POC 資料庫密碼與 blsadmin 管理員密碼寫死在版控中的腳本裡,惟這兩組屬自有測試機、與實際部署環境無關,因此原本「可串成攻擊鏈」的推論前提不成立。仍要改成環境變數,理由是其中兩支的寫法是 os.getenv("...", "<真密碼>")——變數沒設就靜默用寫死的值,這個 pattern 一旦被複製到會連生產環境的腳本上就會出事。
  3. 另外四把外洩的 API 金鑰已經處理掉了(決策者 2026-09-08 當天撤銷重發)。財務風險已解除,但舊字串還散在版控 15 個檔裡(CM-1607),留著會讓未來每一次掃描都撈出一批假陽性。
§2

四張修正卡

卡號 修什麼 嚴重度 現在的狀態
CM-1605 測試信端點不再把存好的 SMTP 密碼配上呼叫端指定的主機;Discord 測試的 SSRF 位址白名單已於 2026-09-08 追加併入本卡 🟠 HIGH ⬜ Not started
CM-1606 SMTP adapter 兩洞:錯誤路徑別把密碼印進 log + starttls 要驗憑證 🟡 MEDIUM×2 ⬜ Not started
CM-1607 清掉版控內殘留的已撤銷金鑰字串(docs/conversation-history/ 13 檔+Trivy 報告 2 檔) ⚪ 衛生 ⬜ Not started
CM-1608 拔掉腳本內硬編的 POC DB 密碼與 blsadmin 密碼(三支腳本,含兩支「環境變數沒設就用真密碼」的靜默 default)|決策者裁:測試機專用、密碼不需更換,本卡只做程式碼衛生 ⚪ 衛生 ⬜ Not started

全部 Not started,順序由 PM 統一安排。 若要排優先序,CM-1605 是唯一的 HIGH,建議優先。 原本建議它與 CM-1608 一起做(同一條攻擊鏈兩端),但決策者已裁定 CM-1608 的密碼屬測試機、與部署無關,該串聯不成立。

§3

兩份完整報告

報告 掃了什麼 找到
N1 套件本體 jedi-notification 套件源碼 30 檔 2 條 MEDIUM,都在 SMTP adapter 同一支檔案
N2 宿主接線 主專案的通知/通知設定接線 18 檔 6 條(2 HIGH/3 MEDIUM/1 LOW),其中 4 條屬範圍外的版控憑證問題
§4

這次的結果可以信到什麼程度

「這幾條存在」可信;「只有這幾條」不可信。

兩棒的驗證面板都完整跑完(stamp 皆為 verified),這是本 arc 至今第一次——前一輪 FR-077 R1 的面板因額度上限全滅,工具正式產出是空的。N2 中途也撞到額度,但工具自己把那條不完整的交棒到下一輪重投,沒有拿部分票充數。

不可信的是覆蓋率:兩棒都是 low effort 的單次快篩,母卡列的重點面有一半沒有結論。尤其那四條範圍外發現,不代表 .env.testdocs/scripts/ 被掃過——那三處從來不是掃描目標。

§5

還沒掃到、可能值得補的

  • 背景 thread 寄信時的租戶邊界(母卡 ⑩)——完全空白。若無 user context 時讀錯租戶的 SMTP 設定,會變成「A 租戶的信用 B 租戶的郵件伺服器寄出去」,屬跨租戶洩漏
  • Telegram bot token 的洩漏面(token 拼在 URL 路徑裡,會進 log 與反向代理紀錄)
  • Discord / Telegram 的 requests.post 沒有 timeout(母卡 ③,可能吊死背景 worker)

上述若要追,範圍都很小(5 檔上下),可併成一小棒。


用 Claude Code 的 claude-security plugin 掃 jedi-notification 這支通知投遞套件, 以及它在主專案裡的宿主接線,找資安問題。 只掃不修——產出是問題清單,修正另外開卡。


這個 arc 在做什麼(白話)

jedi-notification 是產品對外講話的嘴巴。 系統要通知人的時候——寄稽核提醒信、 把工作流事件丟進 Discord 頻道、用 Telegram bot 推訊息——走的都是這支套件。 它掌管三條投遞管道:

管道 用什麼 需要什麼機密
SMTP 寄信 Python 內建 smtplib 郵件伺服器位址 + 帳號 + 密碼
Discord 打一個 webhook 網址 webhook URL 本身即機密(誰拿到誰就能以你的名義發文)
Telegram 打 Telegram Bot API bot token(拼在網址路徑裡)

三條管道的共同特徵:都要存客戶填的機密,都要主動對外連線。 存機密 → 有「機密怎麼被讀出來」的風險;主動連外 → 有「被騙去連別的地方」(SSRF) 與「對方不回應就卡死」的風險。這一輪就是掃這兩類。


為什麼掃這支

§6

決策脈絡

決策者 2026-09-08 裁定 FR-077(遠端 Agent 控制鏈)暫停,改從其他 jedi- 套件開始掃, jedi-notification 是第一支。*

FR-077 的 R1 雖然掃出 7 個真問題,但掃描工具的驗證面板因額度用盡全滅, 工具的正式產出是空的、覆蓋率無法宣稱。與其在同一個大套件上反覆試工具極限, 不如先換到範圍天然較小的套件重新取得可信的完整跑次。

§7

選它的三個理由

  1. 它是「真的獨立」的套件(決策者原話)。與其他 jedi-* 套件耦合少, 掃描時不會一路被牽去讀隔壁套件(FR-077 R1 就因追脈絡讀到 jedi-common, 跑出一條越界發現 F7)。範圍能守得住,結論才乾淨。

  2. 它是至今嘗試過最小的一棒。 全套件排名裡它掛的是「32 檔中小型」, 但實際攻擊面口徑更小:

    口徑 檔數 行數
    整個套件目錄(含 pyproject、README、tests) 67 7,839
    jedi_notification/ 套件源碼(N1 掃描範圍) 30 1,011
    其中扣掉 15 個空的或只有幾行的 __init__.py 15 622

    真正有邏輯的只有十來個檔、六百多行。比 FR-076 L1(10 檔)略大,遠小於 FR-077 R1(42 檔)。

  3. 它同時是「額度可行性實驗」。 決策者目前在 5X token 額度。 手上唯一一次「驗證面板完整跑完」的紀錄是 FR-076 L1(10 檔)—— FR-077 R1(42 檔)的 105 個 verifier 一個都沒活成。 N1 這一棒要回答的問題是:在 5X 額度下,這個工具跑一棒 30 檔還能不能把面板跑完? 若撐不完,代表工具在此額度不可用——這個結論必須在投入更大套件之前先拿到, 所以 N1 一定先派、驗收完才派 N2。


🔴 版本落差提醒(每條發現都要註明)

主專案 pyproject.toml pin 的是 jedi-notification==0.0.11, 且 path override 是註解掉的pyproject.toml:161),也就是說:

  • 掃描讀的是 jedi monorepo 的 HEAD(本機源碼,可能已領先)
  • 部署跑的是 Nexus 上的 0.0.11 wheel

兩者可能不一致。 因此 N1 的每一條發現都必須註明「在 0.0.11 是否同樣存在」, 否則後續修正卡會對不上實際部署中的版本——修了 HEAD 但線上跑的是舊版,等於沒修; 或反過來,某條問題其實 0.0.11 沒有、是 HEAD 新引入的,優先序就完全不同。

驗證方式:runner 掃完後比對 monorepo 該檔在 0.0.11 tag(或該版打包時點)的內容。


首腦讀碼後的重點面

⚠️ 以下是首腦 2026-09-08 實地讀碼標出的可疑點,未經驗證,只是研究員的思考起點。 掃描時不必受限於這份清單,但這幾條務必有明確結論(是問題 / 不是問題 / 為什麼)。

§8

套件側(N1 範圍)

① SMTP starttls() 不驗憑證

檔案infra/smtp_mail/smtp_mail_adapter.py

server.starttls() 呼叫時不帶 SSL context。Python 的預設行為在這種寫法下 不驗伺服器憑證、不驗主機名——等於升級成 TLS 之後,對方是誰完全不查, 中間人可以攔截整條連線(含 SMTP AUTH 的帳號密碼)。

與已修的兩條同一類病:LDAP TLS 不驗憑證(CM-1560)、Redis TLS 不驗憑證(CM-1565)。 同一個 codebase 已經踩過兩次,這是第三處。

② 例外處理把明文密碼印進 log

檔案:同上

except 區塊寫 logger.error(f"...smtp info: {self.config}"), 把整個設定物件印進 log——那個物件內含 SMTP 明文密碼。 正常路徑的 info log 也印帳號與伺服器位址。

log 檔的存取控制比 DB 鬆得多(維運看得到、可能被打包送出、進集中式 log 系統), 密碼落進 log 等於機密的實質存放範圍被放大。

③ Discord / Telegram 的 requests.post 沒有 timeout

檔案infra/telegram/telegram_adapter.pyinfra/discord/discord_adapter.py

兩支的 requests.post 完全沒有 timeout 參數。 Python requests 預設是無限等待——外部服務不回應(或惡意 slowloris)就會吊死呼叫它的 worker

對照:SMTP adapter 那邊有設 60 秒 timeout,這兩支沒有。同一支套件內做法不一致, 更像是漏了而不是刻意。

考慮到宿主有多處在背景 thread 呼叫寄信(見宿主側 ⑩),thread 被吊住的後果要一併評估。

④ 收件人與主旨直接組進郵件標頭(CRLF 標頭注入面)

檔案infra/smtp_mail/smtp_mail_adapter.py

SMTP adapter 直接把收件人與主旨組進郵件標頭(msg['To']Header(request.subject))。 而測試信端點的 to 是使用者可控的自由字串

要看的是:字串裡塞 CRLF(\r\n)能不能在標頭區塊多插一行—— 典型後果是偷加 Bcc: 把信抄送給第三方,或注入額外標頭改變投遞行為。 Python 的 email 套件某些路徑會擋、某些不會,要看實際用到的是哪條路徑

⑤ Discord webhook 是 SSRF 面;Telegram token 拼進 URL

檔案infra/discord/discord_adapter.pyinfra/telegram/telegram_adapter.py

Discord:webhook 網址是使用者填的任意 URL,伺服器會對它發 POST。 使用者填 http://169.254.169.254/...(雲端 metadata)或內網位址, 伺服器就替他去打——這是標準 SSRF 形狀。要看有沒有任何 host 白名單/協定限制。 (對照 FR-077 F4:健康檢查 base_url 未過濾直進 httpx.get,同款。)

Telegram:bot token 直接拼進 URL 路徑。URL 會出現在 log、錯誤訊息、 反向代理紀錄裡——機密走 URL 而非標頭,洩漏面比走標頭大。

register_adapter 無條件覆寫(⚠️ 已知盲點題)

檔案ports/notification_provider/notification_factory.py

register_adapter無條件覆寫——同一個管道名稱後登記者直接蓋掉先登記者。 docstring 明說這是刻意留給宿主替換投遞管道的餘地。

這正是掃描工具已知盲點「會被註解說服」的典型案例。 工具讀到 docstring 說「這是刻意的」,很可能就跳過不報。 要判的是:這個「刻意」在什麼前提下成立(誰能呼叫 register_adapter? plugin 載入順序可不可控?第三方 plugin 能不能藉此把所有通知劫走?), 而不是接受 docstring 的自我宣告。

驗收時要特別確認 runner 對這條有沒有獨立結論,若只是複述 docstring 就是沒掃到。

⑦ 非資安但要記:cc / bcc / attachment 被靜默丟掉

檔案app/dto/notification_request_dto.pyMailRequestDTO)↔︎ infra/smtp_mail/smtp_mail_adapter.py

MailRequestDTO 有 cc / bcc / attachment 三個欄位,但 adapter 完全沒用它們—— 呼叫端填了以為有效,實際被靜默丟棄。

不是資安問題,但屬「靜默失敗」類缺陷(呼叫端無從得知), 掃描順手記下即可,不佔嚴重度名額。

§9

宿主側(N2 範圍)

⑧ 🔴 測試寄信會把既存 SMTP 密碼送到使用者指定的伺服器

檔案app/notification/service/test_mail_service.pysend_test_mail

send_test_mail使用者沒有改密碼的情況下,會從 DB 讀既存的 SMTP 密碼, 合併進使用者這次送來的設定再試寄。

問題在於 host 也是使用者送來的。所以: 持有 smtp-config.update 能力點的人,送一個指向自己 SMTP 伺服器的 host (密碼欄留空不改),就能在自己的伺服器上收到平台的明文 SMTP 密碼。

同款問題 LDAP 已在 CM-1564 修過(同樣是「測試連線時回填既存密碼」的模式), 但 SMTP 這邊看起來沒有跟著修。N2 要確認的是:是真的沒修,還是有別的防護攔住了。

⑨ Discord / Telegram 的同款回填模式

檔案app/notify_config/service/notify_config_test_service.py_resolve_value

_resolve_value 是 ⑧ 的同款模式,作用對象換成 Discord webhook URL 與 Telegram bot token。 要判的是這兩個 secret 的洩漏路徑是否同樣成立(Discord webhook 本身就是機密; Telegram token 拿到即可冒充該 bot)。

⑩ 背景 thread 寄信的租戶設定讀取是否 fail closed

檔案app/flow_control/service/project_service.pyapp/flow_engine/service/workflow_execution_service.py 等十多個消費點

多處用 target=...send_mail_notification背景 thread 呼叫寄信。 背景 thread 沒有 HTTP request,也就沒有 user context——那租戶的 SMTP 設定要怎麼讀?

已知的兩條路徑不一致:

  • 宿主 NotificationService.get_channel_config 有防護(無 context 就 raise)
  • get_notifier 走的是繞 RLS 的 read_root_config_value

要判的是:無 context 時實際走哪條、會不會讀到別的租戶的 SMTP 設定 (進而用 A 租戶的郵件伺服器寄 B 租戶的信,或反過來洩漏設定)。 判準是 fail closed——讀不到就該拒絕,不該退回某個預設值。

⑪ 繞 RLS 的設定直讀是真相來源

檔案infra/system_config/system_config_root_reader.py

整支都是繞 RLS 的直讀,而它是 SMTP 設定的讀寫真相來源。 繞 RLS 的元件本身不必然是問題(系統層讀取本來就需要), 但它的每個呼叫點都要自己負責租戶邊界——要盤清楚呼叫點有沒有漏。

(機制上與 FR-077 R1 的「控制面無 user context、session_scope 走 super admin 繞 RLS」 是同一類,也與 CM-1559 fail-open 同源。)


怎麼拆(兩棒)

scanRoot 範圍 檔數
N1 套件本體 jedi monorepo
~/Projects/Jedicogy/module/jedi-python-package
jedi-notification/jedi_notification/ 全部 30(其中 15 有邏輯,共 622 行)
N2 宿主接線 BE repo
compliance-manager-be
app/notification/service/test_mail_service.pyapp/notify_config/service/notify_config_test_service.py、宿主 notification_service.pyapi/notify_config/routes/serializers/infra/notification/notification_plugin_wiring.pyinfra/system_config/system_config_root_reader.py、`di_containers/notification notify_config`
§10

為什麼分兩棒

掃描工具的 scanRoot 只能指一個目錄,而套件與宿主在兩個不同 repo,物理上就得切開。

更重要的是:風險活在套件與宿主的接縫上。套件把三件事全部外包給宿主—— 「設定從哪讀」(宿主的 config reader)、「密碼怎麼還原」(宿主的 _resolve_value 回填)、 「誰能呼叫」(宿主的能力點守門)。套件自己只負責「拿到設定就投遞」。

所以單看套件會覺得「它只是照吩咐辦事」,單看宿主會覺得「投遞細節在套件裡」, 兩邊各自都看不出 ⑧ 那種問題(DB 密碼 + 使用者可控 host = 密碼送給攻擊者)。 驗收時首腦必須把兩份報告放在一起對,接縫才會浮出來。

§11

派工順序:N1 先,N2 等驗收完

N1 同時是額度可行性的實驗(見上方「選它的三個理由」第 3 點)。 若 N1 在 5X 額度下撐不完驗證面板,代表工具在此額度不可用—— 這個結論要在投入更大套件之前先拿到,所以 N2 不與 N1 平行派


共同設定(沿用 FR-075/076/077 教訓)

項目 為什麼
runner 主 session 模型 Opus 5 (1M context) Sonnet 5(200K)跑大棒必被 180 秒 stall 門檻砍掉
effort low medium 會展開成矩陣派 40+ agent,同檔被多個研究員各讀一遍,額度直接燒光
focus attack-surface 跳過 tests/dist/docs
啟動前 必驗 scope 檔數 git ls-files 對上卡片數字才啟動,對不上代表範圍寫錯
驗收 看 stamp 的 verification 欄 確認面板真的跑完;N1 這一棒此欄是實驗的主要觀測值

進度表

§12

掃描棒

範圍 repo 檔數 面板 找到 報告 卡號 狀態
N1 套件本體 jedi-notification/jedi_notification/ jedi monorepo 30 verified
6/6 票
2(MEDIUM×2) scan-N1 CM-1603 ✅ 已完成並驗收
N2 宿主接線 notification / notify_config / system_config 接線 compliance-manager-be 18 verified
23 票/2 輪
6(HIGH×2 MEDIUM×3 LOW×1;4 條範圍外) scan-N2 CM-1604 ✅ 已完成並驗收

兩棒的驗證面板都完整跑完——這是本 arc 至今第一次(FR-077 R1 面板全滅)。 N2 中途撞額度,工具自己把不完整的那條交棒到第二輪用完整三人面板重投,沒有拿部分票充數。

§13

修正卡(全部 Not started,PM 統一安排)

卡號 修什麼 嚴重度 來源 狀態
CM-1605 測試信端點把平台 SMTP 密碼送到呼叫端指定的主機(F5 Discord SSRF 於 09-08 追加併入) 🟠 HIGH N2 F1+F5 ⬜ Not started
CM-1606 SMTP adapter 兩洞:密碼寫進 log + starttls 不驗憑證 🟡 MEDIUM×2 N1 F1/F2 ⬜ Not started
CM-1607 清掉版控內殘留的已撤銷金鑰(15 檔) ⚪ 衛生 N2 F2 延伸(首腦查證) ⬜ Not started
CM-1608 拔掉腳本內硬編的 POC DB 密碼與 blsadmin 密碼|決策者裁測試機專用、不需更換,只做程式碼衛生 ⚪ 衛生(原判 MEDIUM×2) N2 F4/F8 ⬜ Not started

N2 F2/F3(.env.test 的四把 API 金鑰與 JWT/服務密碼)已於 2026-09-08 由決策者撤銷重發並撤出版控 (commit 5746cef156c1dc43),故不開修正卡。

母卡CM-1602


相關座標

§14

前序掃描 arc

Arc 掃了什麼 路徑
FR-075 jedi-common、jedi-iam(六棒按攻擊面切) docs/features/FR-075-2609-jedi-package-security-audit/
FR-076 License 簽發鏈(license_center + jedi-license-runtime docs/features/FR-076-2609-license-chain-security-scan/
FR-077 遠端 Agent 控制鏈(⏸ 已暫停,R1 掃出 7 條) docs/features/FR-077-2609-remote-agent-security-scan/
§15

方法論與現況

§16

功能背景

§17

文件

以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。

其他文件

文件 類型 標題 最後更新
scan-N1-package-coremd 文件 N1 掃描結果:jedi-notification 套件本體 2026-09-08
scan-N2-host-wiringmd 文件 N2 掃描結果:通知鏈宿主接線(主專案) 2026-09-08
§18

Notion 卡

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

關係 卡號 標題 狀態
母案 CM-1602 FR-078 jedi-notification 通知鏈資安掃描(兩棒,只掃不修)
子卡 CM-1603 N1 套件本體(30 檔) 修正待驗證
子卡 CM-1604 N2 宿主接線(18 檔) 修正待驗證
子卡 CM-1605 修 測試信端點把平台 SMTP 密碼送到呼叫端指定的主機(N2 F1,HIGH) Not started
子卡 CM-1606 修 SMTP adapter 兩洞:密碼寫進 log + starttls 不驗憑證(N1 F1/F2,MEDIUM×2) Not started
子卡 CM-1607 清掉版控內殘留的已撤銷金鑰(conversation-history 13 檔+Trivy 報告 2 檔) Not started
子卡 CM-1608 拔掉腳本內硬編的 POC DB 密碼與 blsadmin 管理員密碼(N2 F4/F8,MEDIUM×2) Not started