範圍:
api/module_frame/routes/ssp_import_template_route.py(169 行)+app/module_frame/service/ssp_import_template_app_service.py(1,500 行),共 2 檔 1,669 行。 掃描工具:Claude Code 官方claude-securityplugin,effort low,focus 生產程式碼。 掃描基準 commit:798a9b5f94a2(工作區有其他 session 的未提交改動)。 驗證章:verified。只掃不修。
已知的那個洞確認成立,而且比原本記的更嚴重——它不只跨專案,是跨客戶;旁邊那兩支同檔方法查過了,一支安全、一支有前提成立才會出事的低風險缺口;另外掃到一條與本功能無關但更急的問題:一組還在用的資料庫密碼被寫進版控,而且跟著文件站公開在網路上。
系統裡有個功能叫「下載 SSP Excel 範本」。SSP 是「系統安全計畫」,一份文件裡寫著一家客戶的系統長什麼樣、誰負責哪一塊、有哪些設備、每條資安控制項他們實際怎麼做。顧問要離線作業時,可以把整份計畫匯出成一個九頁的 Excel 檔帶走。
這一棒要確認的是:這個「帶走整份文件」的按鈕,有沒有檢查你是不是這份文件的主人。
同一支程式檔裡有三個入口:一個從「合規範本」匯出、一個從「某份 SSP」匯出、一個從「框架版本」匯出空白表。盤點時只查過其中一個,另外兩個沒人看過——這棒就是把三個一次看完。
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- B4-1(下載別人的 SSP)=M11 第 1 條,✅ 已修(CM-2188,commit
82618a297,1.21.0 出貨)。- B4-2(資料庫密碼進版控並公開):✅ 已修(CM-2049,commit
5d4c14221;密碼換發排在正式環境上版前統一做)。- B4-3(Excel 公式)=M11 第 20、21 條,✅ 已修(CM-2189,commit
895db0ef8,1.21.0 出貨)。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 嚴重度 |
|---|---|---|---|---|---|
| B4-1 | 下載 SSP 時沒有檢查這份計畫是不是他自己的 | 任何登入者只要拿到別人的計畫編號,就能把別家客戶整份系統安全計畫下載走:人名、email、電話、地址、設備 IP 與 MAC、每條控制項怎麼做 | 有帳號且角色帶「資源庫可讀」權限(預設管理員就有);知道對方的計畫編號(其他 API 會回傳) | app/module_frame/service/ssp_import_template_app_service.py:280 |
高 |
| B4-2 | 一組還在用的資料庫密碼被寫進版控,而且跟著公開文件站發佈到網路上 | 任何看得到 repo 或那個公開文件站的人,都能直接連上 POC(等同正式環境)的資料庫,而且用的是會繞過客戶隔離機制的管理員帳號,讀寫所有客戶的資料 | 連得到那台資料庫的網路(內網/VPN/已進到內網的任何機器) | docs/system-design/scripts/generate_db_schema_docx.py:34(同一組字串另散在約 250 個版控檔中) |
高 |
| B4-3 | 使用者填的文字原封不動寫進 Excel 格子,而檔案被設定成「打開就重算」 | 甲客戶在計畫裡填一段特製文字,乙顧問下載這份 Excel 打開時,那段文字會被 Excel 當成公式執行,把隔壁格的資料送去攻擊者的網站 | 攻擊者要能寫入該份計畫的欄位;要有第二個人下載並打開這個檔;部分手法還要對方點掉 Excel 的信任提示 | app/module_frame/excel_template/generator.py:578 |
中 |
另外兩支被點名要查的方法,查完的結論在第 5 節(一支沒問題、一支有條件性缺口)。
白話說明
網址是 GET /ssp/<計畫編號>/excel-template。程式收到這個編號之後,直接拿去資料庫把那份計畫撈出來,然後產生 Excel 回傳。中間沒有任何一行程式去問「這份計畫是不是這個人的」。
入口那層是有兩道檢查的:一是「有沒有登入」,二是「這個人的角色有沒有『資源庫可讀』這個權限」。但第二道問的是**「你能不能讀資源庫」,不是「這份計畫是不是你的」——這是兩件不同的事。本專案的正規做法是把「是不是你的」這種檢查寫在下一層的商業邏輯程式裡,但這支下一層也沒寫**。
為什麼比原本登記的更嚴重
原本總表第 128 項記的是「跨專案」。實際查過資料庫結構後,範圍更大:
oscal.system_security_plans 等)沒有客戶編號欄位,也沒有開資料庫層的客戶隔離(RLS,就是「每個客戶只能看自己資料」的資料庫機制)。我實際核對過 scripts/init/02-schema.sql:oscal 這個區域底下只有五張「匯入工作紀錄表」有客戶欄位與隔離設定,SSP 本體與它的子表都沒有。在哪裡
| 位置 | 是什麼 |
|---|---|
api/module_frame/__init__.py:231 |
網址登記處 |
api/module_frame/routes/ssp_import_template_route.py:97-116 |
入口,只掛了「要登入」+「資源庫可讀」 |
app/module_frame/service/ssp_import_template_app_service.py:280 |
generate_for_ssp 直接用傳進來的編號撈資料,全檔沒有任何歸屬檢查 |
di_containers/module_frame/module_frame_containers.py:231 |
這支服務的組裝處——連權限檢查器都沒有被裝進來,所以想呼叫也沒得呼叫 |
怎麼修
同專案已經有做對的範例可以照抄:
app/oscal/service/export/ssp_export_app_service.py:74-75app/oscal/service/ssp_control_impl_import_service.py:97兩支都是在方法的第一行就先確認「呼叫的人是不是這份計畫所屬專案的參與者」。做法:把 SspPermissionChecker 加進 SspImportTemplateAppService 的建構參數、在 di_containers/module_frame/module_frame_containers.py:231 補上組裝,然後在 generate_for_ssp 的第一行呼叫參與者檢查,不通過就擋下來。
另外建議(不屬本棒範圍,但這條的影響靠它才收得乾淨):給 oscal 底下的 SSP 相關資料表補上客戶編號與隔離設定。現在的狀況是「程式少寫一行檢查,整個客戶隔離就沒了」,沒有第二道防線。
首腦核對註記:三個獨立檢查員分別從「打不打得到」「出事多嚴重」「有沒有其他機制擋得住」三個角度投票,三票全部認定成立。
白話說明
docs/system-design/scripts/generate_db_schema_docx.py 這支腳本裡,資料庫的主機、埠號、庫名、帳號、密碼全部是直接寫死的文字。這支檔案在版控裡。
更麻煩的是,同一組密碼字串還出現在約 250 個版控檔案中,其中有 67 個是 docs/features/ 底下已經轉成 HTML 的文件——而那批 HTML 是推上 main 就自動發佈到公開網站的(https://guidantai-feature-doc.jedicotech.com/,不需要登入就能看)。
檢查員實際比對過:這組密碼不是過期的舊值,跟現在 .env 和 docker/production/guidant.env 裡在用的是同一組,沒有輪換紀錄。而且依專案自己的文件,這組密碼 cm_app(受客戶隔離限制)跟 cmmgr(系統管理員,設計上就會繞過客戶隔離)共用。
這件事已經違反本專案 CLAUDE.md 第一條「禁止將憑證寫入任何版控檔案」。
出事會怎樣
任何人只要連得到那台資料庫(內網、VPN,或任何已經進到內網的機器),就能用管理員帳號連上 POC 環境——而 POC 依專案定義等同正式環境,是給客戶試玩、對外 demo 用的。連上之後,所有客戶的專案、計畫、使用者、稽核證據都可讀可改,客戶之間的隔離完全失效。
在哪裡
docs/system-design/scripts/generate_db_schema_docx.py:34(密碼寫死處;實際使用在同檔 :1514)docs/features/ 下 67 個已發佈的 HTML檢查員也確認過沒有任何機制擋著:.gitignore 只蓋了 .env 跟 .secrets/,蓋不到這些路徑;沒有 git 過濾器、沒有 hook、CI 裡也沒有掃密碼的步驟。
怎麼修
cm_app 與 cmmgr 都要換——這組值已經進了 git 歷史、也已經發佈在公開頁面上,刪掉那一行不會讓它失效。.env 讀(os.environ["PGPASSWORD"])。首腦核對註記:三票全數認定成立。這條不在本棒原定範圍內(是順著本功能的設定檔追出去的),但嚴重度與急迫性都高於本棒主題,故照實列入。
白話說明
產生 Excel 的時候,程式把使用者填過的文字——聯絡人姓名地址、元件說明、設備欄位、每條控制項的做法敘述、文件檔名——原封不動塞進儲存格。Excel 有個規則:任何以 = 開頭的內容會被當成公式。而這個檔案在產生時被設定成「打開時全部重算」(fullCalcOnLoad = True)。
所以甲客戶在自己的計畫裡填一段以 = 開頭的特製文字,乙顧問下載這份 Excel、打開,那段文字就會被 Excel 執行。
出事會怎樣
攻擊者可以讓公式把隔壁儲存格的內容(也就是這份計畫裡的其他資料)串進一個網址,送到他自己的伺服器;某些寫法在使用者點掉 Excel 的信任提示後,還能在對方電腦上執行外部指令。注意這個執行發生在對方的電腦上,不在我們的系統裡,我們的防護管不到。
為什麼是中而不是高
要三件事同時成立:攻擊者要先有權限寫入那些欄位、要有第二個人下載這份檔案、部分手法還要對方點掉 Excel 的信任提示。不像 B4-1 那樣一個網址就打得到。
在哪裡
app/module_frame/excel_template/generator.py:578(_write_filled_rows)api/oscal/serializers/ssp/ssp_control_implementation.py:42、api/module_frame/serializers/module_frame_party.py:9,16怎麼修
在寫入的那幾個共用函式處理,不要在每個呼叫端各修一次(否則下次新增欄位又會漏)。做法:值是字串且開頭是 =、+、-、@、Tab 或換行時,前面加一個單引號,或寫入後強制把儲存格型別設成文字。要改的三處:_write_filled_rows、_build_vertical_single_row_sheet,以及 lookup 工作表的產生函式——這三支都吃資料庫來的文字。
首腦核對註記:三票全數認定成立。這條正好落在 B4 與 B6 的接縫上——問題的源頭是 B4 把使用者填的內容當成可信任的資料傳下去,真正寫進檔案的動作在 B6 的 generator.py。B6 那一棒會從另一側再確認一次。
盤點時只看過 generate_for_ssp,另外兩支沒人讀到底。兩支都實際開檔讀過了:
generate(範本版,行 222-253)— 沒有同類問題網址 GET /module-frame/<編號>/ssp-import-template,拿範本編號去撈合規範本。
查法與結論:compliance.module_frames 這張表有客戶編號欄位,而且有開資料庫層的客戶隔離(scripts/init/02-schema.sql:24303 起,四種操作各有一條規則)。查詢時資料庫會自動只回傳這個人所屬客戶的資料,別家客戶的範本編號撈出來就是空的,程式會直接回「找不到」。
所以這支雖然一樣沒有在程式裡寫歸屬檢查,但資料庫那一關擋得住。掃描的三個檢查員也沒有對這支提出任何發現。
⚠️ 但要記一筆:這支是靠資料庫隔離在守,不是靠程式。同客戶底下不同專案之間的細分歸屬,這層擋不到;若未來 SSP 那批表補上隔離、或範本表的隔離設定被改動,這支的安全性會跟著變。
generate_by_framework_version(框架版本版,行 599-651)— 摸得到客戶資料,但有限首腦問的是:這支到底是「只查全域框架目錄」(風險低),還是「也會碰到客戶資料」(風險高)。答案是後者,但摸到的範圍有限。
拆成兩半看:
控制項清單那一半:純全域,沒問題。 framework_version_uid → oscal.framework_versions → catalog_id → 該框架的全部控制項。oscal.frameworks 與 oscal.framework_versions 兩張表都沒有客戶欄位、也沒有隔離設定——因為它們本來就是全域共用的產品資料(CMMC、ISO 27001 這些框架,每家客戶看到的都一樣)。這部分是唯讀的公用目錄,沒有跨客戶問題。
下拉選單那一半:會撈到呼叫者所屬客戶的實際資料。 行 640 呼叫 _fetch_lookups_without_mf(),它會撈(行 676-689):
| 撈什麼 | 撈到的內容 | 對應程式 |
|---|---|---|
| 使用者 | 該客戶全部使用者的暱稱與帳號(email) | :809 |
| 部門 | 該客戶全部部門名稱 | :816 |
| 設備 | 該客戶全部設備的名稱與 IP | :849 |
| 資訊系統 | 該客戶全部資訊系統的簡稱與名稱 | :861 |
| 範本程序書 | 空的(這支刻意留空) | :678 |
另外行 641 的 _fetch_lookups_helpers() 還會多帶使用者的 email 與姓名、設備的 IP 與作業系統。
這算不算漏洞?結論:不算跨客戶外洩,但是「權限粒度過寬」的缺口。
我核對過這四張表(public.users、public.org_units、public.devices、compliance.information_systems)都有開客戶隔離,所以撈到的一定是呼叫者自己公司的資料,拿不到別家客戶的。
但問題在於:這支的入口只要求「資源庫可讀」權限。一個只被授予「看合規範本」的低權限使用者,可以用這支下載一份空白範本,而這份空白範本的下拉選單裡會附上全公司的使用者 email、全部設備的名稱與 IP、全部資訊系統清單。這在滲透測試裡是很好用的內部偵察材料——雖然不跨客戶,但它讓一個本來只該看到範本的人,拿到了全公司的資產與人員清冊。
三個檢查員沒有把這條提為獨立發現(掃描重點在跨客戶那條),這是我實際讀程式後補的判斷,屬中低風險,是否要收成修正卡請首腦裁示。 若要修,方向是把下拉資料的取得改成依呼叫者實際權限縮減,而不是一律給全客戶範圍。
分兩層講,這兩件事不一樣:
「這三條存在嗎」——可信度高。 每一條都由三個角度不同的獨立檢查員各投一票,九票全數投出、沒有漏投,三條都是三票全數認定成立,沒有任何一條是靠少數票勉強過關的。而且三條的關鍵事實我都另外自己開檔核對過:B4-1 的資料表隔離設定、B4-2 的密碼是否還在用、B4-3 的寫入位置,都對得上。
「只有這三條嗎」——不保證。 理由:
generate_by_framework_version 的權限過寬問題,是我讀程式讀出來的,掃描工具沒報——這直接說明工具會漏。| 項目 | 數值 |
|---|---|
| 掃描範圍 | 2 檔 / 1,669 行 |
| 基準 commit | 798a9b5f94a2(工作區 dirty,有平行 session 改動) |
| 檔位 | effort low,focus 生產程式碼 |
| 研究員 | 派 2 支,回 2 支 |
| 原始候選 | 5 條 → 去重後 3 條 |
| 投票 | 三個檢查員 × 3 條 = 9 票,全數投出 |
| 票型 | F1 3:0、F2 3:0、F3 3:0(無任何一條被降級) |
| 驗證章 | verified |
| 耗時 | 約 91 分鐘 |
| 工具產出原始報告 | CLAUDE-SECURITY-20260922-090440/(未入版控) |
generate_by_framework_version 權限過寬要不要收成獨立卡。