範圍:套件 jedi-oscal-v2 的 1 檔/1,660 行:
jedi_oscal_v2/app/service/io/oscal_io_service.py。 掃描工具:Claude Code 官方claude-securityplugin,effort low,scoped 掃描。 基準 commit:套件 repoeafc7ae511d5(feature/review,工作區有平行 session 的未提交改動,stamp 標 dirty;範圍內這一支檔乾淨)。 驗證章:verified,0 條(研究員沒提出任何候選,面板沒有票可投)。只掃不修。
這一棒沒有新的資安問題。 工具零發現;卡片點名的七件事,runner 逐條跨 repo 開檔追完,全部不成立,或是總表已登記的舊案。
另外追出 1 條非資安的真 bug(V3-1):專案 SSP「再匯入一次」時,人員、實作項目的 uuid 每次都被換成新的,外部服務與設備每次都多一份重複。不會跨份讀寫,但會讓舊的內部參照指到不存在的人。
這支檔是整個套件唯一的「整份進、整份出」:
Word/Excel 上傳 ─(W1/W2/Excel 解析)─→ 解析單 ─使用者按確認─→ import_adapter 組成 OSCAL 字典
─→ import_ssp(本檔)逐筆寫進 45 張沒有客戶隔離的表
資料庫裡的一份文件 ─→ export_oscal(本檔)─→ OSCAL JSON(給外部工具)
它是 45 張零隔離表的批次寫入口,所以要回答四件事:
背景:這 45 張表沒有客戶欄位、沒有資料庫隔離(總表第 128 項,盤點檔第 5 節;本棒 DEV 唯讀重查 oscal.system_security_plans、oscal.parties 兩張隔離仍為關,2026-09-24 18:58)。能不能跨客戶讀寫,百分之百看呼叫端有沒有先從有隔離的表往下找。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度+為什麼 | 來源 |
|---|---|---|---|---|---|---|
| V3-1 | 非資安真 bug:專案 SSP 再匯入時,uuid 每次換新、外部服務與設備每次重複 | 人員的 uuid 被換掉,原本指著它的「外部服務提供者」變成指向不存在的人(DEV 604 筆外部服務有 17 筆提供者查無此人);外部服務、設備清單每匯一次多一份 | 專案負責人對同一份專案 SSP 用 Word 或 Excel 再匯入一次(正常操作,不是攻擊) | 套件 oscal_io_service.py:998-1000(人員)、:1292-1295(實作項目)、:1326-1329(實作敘述):更新時已配對到舊列就一律沿用舊 uuid,不看字典有沒有帶;:1183-1235 外部服務與設備改用自然鍵(標題/描述)配對,或由主專案在組字典前先套上現有 uuid |
不是資安:不跨份、不跨客戶、沒有權限繞過;是資料正確性問題 | runner 開檔追出、未經三人面板投票;DEV 唯讀佐證 |
工具報的:0 條。 研究員讀完整支檔、密鑰專項也跑完,兩者都沒有提出候選。
無。零發現不等於乾淨,下一節是 runner 照卡片逐條追的結果。
這一條依首腦補充改問:套件信任呼叫端給的 uuid(:982 uuid=p.get("uuid") or self._gen_uuid()),只有在使用者能把 uuid 放進字典時才是洞。追了全部呼叫端:
① import_ssp 只有兩個呼叫者,都在主專案。
app/oscal/service/ssp_docx_import_app_service.py:361、:385、:509(Word)。app/oscal/service/ssp_excel_import_app_service.py:614(Excel)。② 兩條路組字典的地方,uuid 全部是伺服器產生。
docx_to_oscal_ssp.py:41/69/97)。excel_to_oscal_ssp.py:36(組織)、:51(個人)、:84(實作項目)、:98(實作敘述),全部 _common.gen_uuid()。_common.py:
:281、:331;:401-404;:466、:497;:579。gen_uuid()=uuid.uuid4()(:24-25)。③ 使用者在確認時能改的東西,改不到人員 uuid。
Excel 確認時的 content_overrides 比 Word 寬:_apply_content_overrides(ssp_excel_import_app_service.py:694-695)對六張工作表的任一列可以蓋任意欄位(rows[idx][k] = v)。逐一看它能影響什麼:
| 使用者塞的欄位 | 到了組字典那一層 | 結論 |
|---|---|---|
uuid(任何一張表) |
組字典時不讀這個鍵,一律 gen_uuid() |
改不到 |
人員的 matched_user_id |
存成人員的 matched-user-id 屬性(_common.py:121-123) |
跟手動編輯人員(module_frame_party_service.py:317-319)是同一種能力;讀回時查 users 表,而 users 表 DEV 隔離為開,反查不到別家 |
設備的 implemented_components 塞成 [{"component-uuid": 任意值}] |
_resolve_implemented_components 原樣放行(_common.py:519-520) |
跟手動編輯設備(ssp_inventory_items_app_service.py:91)同一種能力;讀回時只在本份 SSP 的元件清單裡對標題(module_frame_inventory_service.py:134-135),對不到就顯示空白,讀不到別份 |
④ 就算 uuid 重複,也沒有地方「用 uuid 全表找人」。
ssp_party_app_service.py:136-140;module_frame_party_service.py:166-170;ssp_components_app_service.py:140;ssp_leveraged_app_service.py:139;ssp_inventory_items_app_service.py:182;PartyQueryEntity(uuid=…) 這種全表查詢。oscal.parties 裡「同一個 uuid 出現在兩份以上 metadata」= 0 筆。結論:地雷還在,但目前沒有引信。將來如果開放「上傳 OSCAL JSON」,要在組字典前把使用者給的 uuid 全部換掉,或在套件 import_ssp 一律自產。
套件確實不管大小:import_ssp 對每個陣列逐筆 add(),而共用底層 BaseRepositoryImpl.add 每筆都 flush()(jedi_common/.../base_repository_impl.py:401-403),等於每筆一次資料庫來回。所以做了三組實測:
① 寫入次數(假資料庫,只數次數):一份字典放 N 個人員、N 個元件、N 個設備、N 個實作項目(每項各一個實作敘述、兩個說明掛點)。
| N | 模式 | 寫入次數 | 讀取次數 | 純 Python 耗時 |
|---|---|---|---|---|
| 1,000 | 建立 | 7,003 | 4 | 0.01 秒 |
| 5,000 | 建立 | 35,003 | 4 | 0.06 秒 |
| 20,000 | 建立 | 140,003 | 4 | 0.34 秒 |
| 20,000 | 更新 | 140,003 | 60,009 | 6.5 秒(假資料庫每次回全部列,真資料庫只回同層幾筆,實際會更快) |
寫入次數是線性的,沒有平方級。
② 單筆成本(DEV 本機,暫存表,最後 ROLLBACK):
INSERT … RETURNING:0.07 毫秒;add 再 flush」:0.11~0.12 毫秒,2 萬筆 2.3 秒。③ 換算:14 萬筆寫入約 16 秒。資料庫在另一台機器、來回慢十倍,也在 120 秒逾時的量級附近,而且這還是 N=2 萬的極端值。
實際上 N 到不了 2 萬:
excel_parser/sheet_handlers.py:115-118 max_row_cap=5000),上傳限 10MB(ssp_excel_import_app_service.py:47)。結論:大小的防線在主專案那一層(每表 5,000 列+解析逾時),套件這層沒有、也不需要另加。若將來開放 OSCAL JSON 匯入,要在那個入口自己限陣列長度。
clear_ssp_body 的 SSP 編號是誰給的:套件範圍對,缺口在主專案=總表第 123/125 項(既有案,不另計)現況:既有案,不另計;V3-1 再匯入換新編號已修(M11-39,FR-114 CM-2185,套件 commit 1d3a59c8,1.21.0 出貨)
clear_ssp_body(:281-319)只刪這份 SSP 的三大塊,加上這份 metadata 底下的人員與角色:
get_by_ssp(ssp_id);PartyQueryEntity(metadata_id=…);RoleQueryEntity(metadata_id=…)。子表靠外鍵連鎖刪除,不會刪到別份。
兩個呼叫端的編號來源:
ssp_excel_import_app_service.py:464-478:job.source_uid → get_resource_library → ssp_template_id。ssp_docx_import_app_service.py:334-337 → :384:job.source_uid or payload.get("source_uid") → 同上。get_resource_library 只問「存在嗎」,不問「你能不能改」。資源庫表 DEV 隔離為開,但讀取規則放行原廠公版(scope='SYSTEM')與母公司分享(SHARED),所以讀得到的範圍包含不屬於你的範本。這就是:
_verify_source_exists 與 _confirm_update_module_frame 無守門);本棒補一句「套件丟出來的長什麼樣」:套件照單全收呼叫端給的編號、先清再建,第二道防線不存在。修法照第 123/125 項,在主專案補守門即可,套件不用動。
① 範圍都對。 四支 _export_* 都先用 uid 撈根文件,之後所有子物件都用根文件的數字編號往下撈:
:531-649,catalog.id;:652-670,ssp.id/ssp.metadata_id;:1398-1417,ar.id → 每個 result 的 result.id → risk 的 risk.id;:1557-1613,poam.id。by-component 的 component-uuid 用本份 SSP 的元件對照表反查(:844-852),沒有全表查詢。
② 唯一有網址的是目錄下載:api/oscal/routes/framework/oscal_framework_version_route.py:118-143,短效憑證綁版本 uid,FR-113 O8b 已看過。
③ 另外三種沒有入口。 OscalExportAppService.export(app/oscal/service/oscal_export_app_service.py:29)能接四種文件,但主專案只有上面那一處呼叫它、固定傳 'catalog'。
它的檔頭註解寫「本期權限:jwt 認證即可;per-project 參與者授權為 follow-up」。將來若有人為 SSP/稽核結果/改善計畫加匯出網址,不能直接接這支——它不檢查文件屬於誰,而底下的表沒有隔離。記地雷,不另計。
④ build_ssp_snapshot(:257-278):同樣以 SSP 編號往下組,呼叫端的編號來源同 5.3。預覽時把目標範本現有內容回傳給使用者,這是第 125 項「動手前還能先讀」那一半,既有案。
.commit() 零筆;本檔也沒有。@transaction:
ssp_excel_import_app_service.py:302;ssp_docx_import_app_service.py:303。.commit()、零 session_scope()。clear_ssp_body → import_ssp → 還原程序書關聯三步在同一個交易裡。還原那支(ssp_document_pool_service)雖然也標了 @transaction,但共用底層在「已經在交易裡」時直接沿用、不另開(jedi_common/.../db.py:230-241)。:393-401)。Word 那條接住這個錯誤、改走「清空再建」時,前面沒有半筆寫入要撤。套件一律 raise ValueError(f"... id={...!r}"),訊息帶內部數字編號,例如:
:389 target ssp not found: id=…;:277、:534、:655。追三個接收點:
| 接收點 | 怎麼處理 ValueError |
前端看到什麼 |
|---|---|---|
ssp_excel_import_app_service.py:620-630 |
msg = str(e) 只用來判斷是不是「already has a body」,然後寫日誌 |
固定錯誤碼 GRC_IMPORT_SSP_TEMPLATE_NOT_EMPTY 或 GRC_EXCEL_INVALID_FILE,不帶原文 |
ssp_docx_import_app_service.py:363-366、:515-520 |
寫日誌 | 固定錯誤碼 GRC_DOCX_PARSE_FAILED |
oscal_export_app_service.py:43-45 |
寫日誌(info) |
固定錯誤碼 GRC_EXPORT_DOC_NOT_FOUND |
卡片提到的 ssp_docx_import_app_service.py:220,226 寫進解析單並回前端,是解析階段的例外(第 132/174 項同形、O4 範圍),不是本檔丟的。
by_component 掛在哪個元件底下:只會掛到本次寫入的這份 SSP 的元件(下次不用重查)_import_by_component(:1342-1395)用 comp_map.get(bc["component-uuid"]) 找元件編號,查不到就跳過、寫日誌。comp_map 只有三種來源,全部限定在本份 SSP 的系統實作 new_si.id 底下:
:1166);:1174-1175);:1179-1181,來自 get_by_system_implementation(new_si.id))。別份 SSP 的元件 uuid 就算被塞進字典,也對不到,會被跳過。
更新模式的自然鍵(:362-372):每一種都帶父層編號。
metadata_id;system_implementation_id;control_implementation_id;implemented_requirement_id;system_characteristics_id。「更新」時先用這些鍵在本份底下撈既有列,再把 entity.id 設成撈到的那筆。不會跨份配對。
共用底層的 update 只看 id 找列(base_repository_impl.py:413-417,entity.uid 在這些實體上不存在時走 id)。本檔設進去的 id 全部來自上一行的「本份底下撈到的列」,沒有從字典讀 id。字典裡就算塞了 "id": 123 也不會被讀(本檔所有建構實體的地方都沒有 .get("id"),角色的 r.get("id") 是 OSCAL 的角色代號、存成 role_id)。
所有 JSON 欄位原樣存(props、links、remarks、responsible-parties 等):沒有深度或長度檢查。來源是主專案組好的字典,巢狀深度由組字典的程式決定、使用者控制不了結構;長度受 Excel 每格內容與 10MB 上限約束。匯出時原樣吐回,公式注入的問題在匯出 Excel 那端(第 126/148 項),不在 JSON 匯出。
_poam_owned_query(:1621-1645)用類別名稱字串判斷要建哪種查詢條件:寫法脆弱(改名就壞),但三個呼叫都傳固定的 repo,不是輸入。不算問題。
情境:專案負責人在「專案 SSP」頁把同一份 Word 或 Excel 再匯入一次(source_type='ssp',Model C,更新模式)。
發生什麼:組字典的那一層每次都產生全新的 uuid(5.1 ②)。套件更新模式配對到舊列之後,對 uuid 的處理不一致:
| 物件 | 配對用的鍵 | 配對到舊列時 uuid 怎麼處理 | 結果 |
|---|---|---|---|
| 元件 | 標題+類型 | 一律沿用舊的(:1157-1162,註解寫明是為了讓參照不斷) |
✅ 穩定 |
| 人員 | 類型+名稱 | 字典有帶就換成新的(:999-1000 只在字典沒帶時沿用) |
❌ 每次換新 |
| 實作項目、實作敘述、說明掛點、資訊類型 | 控制項代號/敘述代號/元件/標題 | 同人員(:1294、:1328、:1391、:1092) |
❌ 每次換新 |
| 外部服務、設備 | uuid 本身(:1186、:1213) |
新字典的 uuid 永遠對不到 | ❌ 每次多一份 |
後果:
party_uuid,軟參照、沒有外鍵)。人員 uuid 被換掉後,舊的外部服務指向不存在的人。DEV 唯讀(2026-09-24 19:0x):604 筆外部服務有 17 筆提供者在整張人員表查無此 uuid。
source_type='ssp' 的 completed 都是 0),所以這些重複的來源無法證實是這條路。本條以讀程式碼推論為主,DEV 無法直接重現。為什麼不是資安:全部發生在同一份 SSP 裡,沒有跨份讀寫、沒有權限繞過;觸發的是有權限的負責人做正常操作。
修法方向:
建議修在套件,一處修好、Word 與 Excel 兩條都受益。
「這幾條存在嗎」:高。
「只有這幾條嗎」:中偏高。
import_diff/ 目錄沒有逐支通讀(FR-113 O9 範圍),只讀了跟 uuid 與決策合併有關的部分。| 項目 | 值 |
|---|---|
| run ID | wf_94fad4ad-8c6 |
| 報告目錄 | 套件 repo CLAUDE-SECURITY-20260924-105441/(不入版控) |
| stamp | CLAUDE-SECURITY-REVISION-eafc7ae511d5-dirty.json |
| verification.status | verified(reason_kind 無;候選 0,面板 0 票) |
| 形狀 | low:1 名研究員讀 1 檔+1 次密鑰專項(focus=attack-surface) |
| 候選 → 通過 | 0 → 0 |
| agent 數/失敗 | 2/0(journal.jsonl 無 failed,沒撞看門狗) |
| 耗時 | 掃描約 2 分 20 秒;人工追查與實測另計 |
| 人工實測 | 假資料庫數寫入次數(N=1k/5k/20k,建立與更新);DEV 本機暫存表量單筆成本(5,000 次來回、3.5 萬筆 INSERT、2 萬筆 ORM add+flush,全部 ROLLBACK) |
| DEV 唯讀實查 | 2026-09-24 18:58~19:08:oscal 兩表與資源庫表隔離狀態、module_frames 規則原文、人員 uuid 跨 metadata 0 筆、外部服務提供者查無 17/604、設備與外部服務同份重複 5/2 組、兩張解析單表完成的專案 SSP 匯入 0 筆;全程 BEGIN READ ONLY … ROLLBACK |
import_ssp 信任字典裡的 uuid 與陣列長度;OscalExportAppService.export 對 SSP/稽核結果/改善計畫不檢查文件歸屬。