檢查日期 2026-09-25|對應卡片 CM-2154(母卡 CM-2152)|範圍 6 個檔案、1,233 行|工具 run ID
wf_2ac3d75c-1b1
這一層沒有「不必登入就能讓全產品停擺」的第二條(第 154 項那一類),但查到三個新的中度問題,都是「門開了一道縫」,不是「門沒鎖」。
卡片交代要帶著第 154 項的思路去找兩件事,結果如下:
新查到的三個中度問題:
/static/ 底下,不必登入就能下載。(新,中)
/static/file/answer/upload/<帳號>/<時間戳>.xlsx,意見回饋附件、OSCAL 匯出檔也存在這裡。/api/1.0 與 /socket.io 轉給後端,所以正式安裝版從外面打不到。/ 會不會被當成讀取放行」,核對後不成立(只會多擋、不會漏放),細節見 U2-3。掃描工具報了四條,全是已經在總表上的舊帳:
所以四條都不另外計數。只有一件事值得首腦注意:
jedi-iam 1.3.0 裡沒有這段。我開 1.3.0 的正式安裝檔核對過,確實沒有。每一個打進後端的請求,在碰到任何功能之前,都會先經過這一層。這一層決定:
錯一步就是全站遭殃。
| 檔案 | 行數 | 角色 |
|---|---|---|
core/app_factory.py |
469 | 把整個後端組起來:掛上各道關卡、決定掛載順序、公開哪個目錄 |
main.py |
302 | 唯一的啟動入口:切換 API/即時通訊/診斷三種模式、內嵌網頁伺服器 |
common/middleware/license_readonly_mw.py |
217 | 授權到期後「全站只能看、不能改」的閘門 |
config/config_loader.py |
123 | 啟動時檢查必填設定(資料庫、權杖簽章金鑰等)有沒有填 |
core/host_capabilities.py |
76 | 主專案自己的功能權限清單(純資料表) |
common/middleware/jwt_mw.py |
46 | 登入權杖檢查的轉接殼;真正的檢查在身分套件 jedi_iam 裡 |
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度+為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U2-1 | 使用者上傳目錄整個公開在 /static/,不必登入 |
問卷答案 Excel(含受稽核單位的作答內容)、意見回饋附件、OSCAL 匯出檔可被任何人下載 | 後端 8000 埠對外可達(排錯時臨時開埠、或換掉出貨的 nginx);還要猜得到檔名(帳號名+到微秒的時間戳,或意見回饋的 UUID) | core/app_factory.py:94-98:Flask 內建的靜態目錄指向使用者上傳區 |
中:影響是跨公司的資料外洩,但正式安裝版被 nginx 擋住,而且要猜檔名 | runner 自行開檔+讀 nginx 設定 |
| U2-2 | 即時通訊服務逐筆記錄封包,登入權杖明文進容器紀錄,診斷包不遮罩帶走 | 拿到診斷包的人(回廠支援、轉寄鏈上的任何人)可以冒用最近幾天連過線的使用者,效期約 83 小時 | 客戶產過一次診斷包,而且診斷包落到不該拿的人手上 | core/app_factory.py:184,186(logger=True、engineio_logger=True);infra/support/collector/host_collector.py:136-156(容器紀錄沒過 mask_log_lines) |
中:不必攻擊、正常維運就會發生;需要拿到診斷包 | runner 實測 |
| U2-3 | 唯讀閘門的兩個縫:代理程式註冊碼、問卷填答通道 | 授權到期(或被人為停權)的公司仍能產生代理程式註冊碼、仍能改問卷答案 | 登入帳號;註冊碼那條要管理員 | common/middleware/license_readonly_mw.py:76(/agents/ 前綴太寬);問卷通道在 jedi_survey 的 fill_survey_socketio_handler.py:164 |
中低:影響是商務執法破功,不是跨公司資料外洩 | runner 實測 |
| — | 資料庫與 Redis 密碼寫在進版控的文件裡 | 見總表第 12 項 | 見總表 | 見總表 | 中(面板把研究員的「高」降為中) | 工具 F1=總表第 12 項,不另計 |
| — | Google 雲端硬碟應用程式密鑰與 API 金鑰寫在文件裡,而且到現在還在用 | 見總表第 74 項 | 見總表 | 見總表 | 中 | 工具 F2=總表第 74 項,不另計 |
| — | 帳號停權後,手上的通行證還能用到過期(約 83 小時),還能自動續期到約 11 天 | 見 M13 第 17 條 | 見 M13 | 出貨的 jedi-iam 1.3.0 沒有停權檢查 |
中 | 工具 F3=M13 第 17 條(CM-2056),不另計 |
| — | OpenAI/Anthropic 金鑰與 GitLab 通行證寫在文件裡(已撤銷) | 見總表第 10、11 項 | 見總表 | 見總表 | 低 | 工具 F4=總表第 10/11 項,不另計 |
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- U2-1(
/static/公開)=總表第 185 項,✅ 已修(CM-2219,commit782ebb8b3,1.21.0 出貨)。- U2-2(封包進紀錄與診斷包)=總表第 186 項,✅ 已修(CM-2212,commit
c8534bfcb/8f5755f1d,1.21.0 出貨)。- U2-3(唯讀兩個縫)=總表第 187 項,✅ 已修(CM-2217,commit
3ed47c7a9/套件8ccfac73,1.21.0 出貨)。- 工具 F1~F4 舊帳:總表第 12 項 ✅ 已修(CM-2049)、第 74 項 ✅ 已修(CM-2051)、M13 第 17 條 ✅ 已修(CM-2056)、第 10/11 項 ✅ 已修正。
四條都經過三人面板投票。全部是已知舊帳,本棒不另外計數,但各有一件新確認的事實可以補進總表:
研究員原本評「高」,三位檢查員一致改評「中」。
新確認的事:
.env 裡還是現行的資料庫密碼與 Redis 密碼。docs/claude/memory/ 下兩個檔:project_ssp_doc_parser_progress.md:101、project_drive_sync_test_data.md:18。後者連主機、埠、庫名都寫齊了。docs/claude/memory/ 是 2026-09-06 起入版控的共享記憶目錄。這表示 FR-114 清密碼殘留時,不能只清 docs/conversation-history/,還要清 docs/claude/memory/。新確認的事:
.env 裡跟文件裡的是同一組,沒有換過。新確認的事:
jedi-iam 修正線的 commit 3a96260b 裡,但出貨釘住的 jedi-iam==1.3.0 沒有它。jedi-iam/dist/jedi_iam-1.3.0-py3-none-any.whl 核對,middleware/core.py 裡找不到 _is_account_disabled。poetry.lock 裝出來的卻沒有。這個落差修正線發版時要記得。.env 用的也是別的值。維持原判。.env 內容被貼進對話紀錄,裡面有登入權杖的簽章金鑰。scripts/installer/install.sh:1259)。我同意。license_readonly_mw.py:它怎麼判斷「這是寫入」?白名單是前綴比對還是完整比對?——🟡 部分成立,見 U2-3判斷方式(_should_block,第 157-165 行)分三段:
這個「預設擋、例外放」的方向是對的。檔頭註解也說清楚了為什麼不用「看網址長相猜」:失效方向會變成放行寫入。
三種比對方式各查了一遍:
| 名單 | 比對方式 | 查核結果 |
|---|---|---|
豁免名單 _EXEMPT_PREFIXES |
前綴比對 | 🟡 /agents/ 太寬,見下 |
讀取白名單 _POST_READ_PATHS |
完整比對 | ✅ 網址結尾多一個 / 就對不上,但方向是多擋不是漏放(見 U2-3 第 3 點) |
讀取前綴 _POST_READ_PREFIXES |
前綴比對,並用「結尾是 /reset」排除寫入 |
✅ 逐支核過:/notify-config/<通道>/test 只寄測試信;/detection-tools/configs/<uid>/test-connection 只測連線;/reset 被排除(實測 block);/<uid> 本身是 PUT,本來就擋 |
讀取樣式 _POST_READ_PATTERNS |
正規表示式 | ✅ 四條都用 ^…$ 鎖住頭尾;逐條對照實際路由,比對到的都是 list/verify/validate |
豁免名單的前綴逐一對過實際路由(主專案+所有 jedi 套件的路由表):
/login、/logout、/refresh、/otp-*、/totp-*、/forget-password、/user/change-pwd-by-req:都是登入或找回密碼流程,豁免合理。
/login 用前綴比對,所以 /login-anything 也會放行(實測 PASS);但目前沒有任何路由長這樣。/license/:解除唯讀的唯一途徑,合理。
lock/unlock/assign/lc-issue 等後台操作,也跟著豁免。但這些本身要平台管理員,而平台管理員本來就不受唯讀影響(viewer_is_readonly() 對平台管理員恆回 False),沒有擴大攻擊面。/setup/:首次安裝精靈,另有設定碼守門,U3 已確認。/webhooks/:只有 Google 雲端硬碟變更通知一支,由 Google 呼叫,合理。/agents/:註解說是「機器對機器(mTLS)」,但這個前綴底下混了兩支人操作的寫入:
POST /agents/enroll-token:產生代理程式註冊碼,會寫資料庫;POST /agents/enroll-token/revoke:停用註冊碼,會寫資料庫。jedi_remote_agent/api/routing.py:42-43,第三欄 True=要登入)。_should_block("POST", "/agents/enroll-token") 回 False,也就是放行。/remote-agents 新增 agent 則被擋(實測 block)。同一個模組兩條寫入路徑,一條擋、一條不擋。有沒有「POST 其實是讀」被誤擋、或「GET 其實是寫」被漏放?
jwt_mw.py:權杖過期、簽章錯、缺欄位時怎麼處理?有沒有把「解不開」當成「匿名放行」?——✅ 不成立這支 46 行只是轉接殼,把「怎麼拿到使用者服務」交給身分套件。我跨到身分套件 jedi_iam.middleware 讀了真正的檢查,並且對照了出貨的 1.3.0 安裝檔:
| 情況 | 處理 | 依據 |
|---|---|---|
| 沒帶權杖 | 401 | jwt_mw.py:141-144 unauthorized_loader |
| 簽章錯、格式壞 | 401(TOKEN_INVALID_TOKEN) |
invalid_token_loader |
| 過期 | 401(access 和 refresh 分開回碼) | expired_token_loader |
| 已登出(在撤銷清單上) | 401 | token_in_blocklist_loader 查資料庫 |
| 權杖所指的使用者查不到 | 401 | core.py 拋 user_not_found → user_lookup_error_loader |
缺 uid 欄位 |
401 | core.py:86-89:uid 是空的就不查,直接當查不到 |
X-Tenant-ID 指向自己不隸屬的公司 |
403(GRC_403063) |
context.py:110-112;1.3.0 安裝檔裡有這段,已核對 |
X-Tenant-ID 不是數字 |
400 | context.py:108 的 int() 失敗 → invalid_tenant_override |
| 帳號已停權 | ⚠️ 1.3.0 沒擋=工具 F3=M13 第 17 條 | 見上 |
return 回應(不是 raise),所以不會變成「500 而且沒有 CORS 標頭」這種前端讀不到錯誤碼的情況。檔頭有說明為什麼。X-Tenant-ID: 0 以前能讓整個資料庫的客戶隔離失效。
jedi-common 1.2.0 判斷「超級管理員」的條件已經改成「系統身分」或「權限路徑包含原廠根節點」(db.py:193-196,1.2.0 安裝檔已核對)。tenant_id=0 已經不會被推成超級管理員。兩道都在。core/app_factory.py:中介層掛載順序+哪些路由被豁免——🟡 順序正確,但發現公開目錄問題,見 U2-1掛載順序(before_request 依註冊順序執行):
init_request_context_isolation:清掉上一個請求殘留的身分。註解要求它必須最早,實際也是最早(:248)。:253):只看 Content-Length 標頭,不讀內容、不需要身分。init_app_interceptor(app_mw.py,第 154 項所在,已由別棒報過)。:350):在 DI 組好之後才掛,所以一定晚於第 1 步,不會讀到上一個請求的身分。✅豁免清單:
@jwt_required()。method_decorators = [jwt_required()] 統一掛,有守。🟡 本棒新查到的:公開目錄(U2-1)
app_factory.py:94-98 把 Flask 的靜態目錄設成 resource_root()/static,網址是 /static,而且不需要登入。問題是這個目錄正是使用者上傳檔案落地的地方:
docker/production/Dockerfile:173-177 的註解寫明「/app/static 使用者上傳落地處」。docker-compose.yml:147 把它掛成資料卷。static/file/answer/upload/<帳號>/<時間戳>.xlsx(jedi_survey/api/routes/task_survey_route.py:141-150)。static/ 目錄裡確實有 feedback/<uuid>/<時間戳>.jpg、file/answer/upload/<帳號>/、oscal/excel|pdf|upload/。為什麼正式安裝版目前打不到:
nginx.onprem.conf:88,120,145)只把 /api/1.0 和 /socket.io 轉給後端,其他網址都當成前端頁面處理。expose 開在內網。什麼情況會打到:
ports: ["8000:8000"]」(docker-compose.yml:288-289);python main.py(聽 8000);檔名猜不猜得到:
建議:static_folder 改指向只放隨版唯讀資源的地方(resource_path("resources"),config 裡已經有 RESOURCE_DOWNLOAD_DIR),或乾脆設 static_folder=None。使用者上傳區不該透過免登入的內建路由對外。
main.py 的 diag 子命令:只在本機命令列觸發、網址打不到嗎?——✅ 成立(安全)diag 分支(main.py:82-87)只看 RUN_MODE 環境變數,在任何網站路由載入之前就 raise SystemExit 結束,不起服務。POST /support/diagnostic-bundle,要登入而且要平台管理員(diag_bundle_export_service.py:74 require_platform_admin()),屬 U10。在身分檢查之前會碰到請求內容的,逐一列出:
| 在哪 | 做什麼 | 會不會卡住系統 |
|---|---|---|
請求大小上限 app_factory.py:427-432 |
讀 Content-Length 這個數字 |
不會:只比一個整數 |
唯讀閘門 _should_block |
拿網址去比對 4 條正規表示式和幾個集合 | 不會:實測 8KB 惡意網址(重複 800 次 /jobs/list、8,000 個斜線),100 次平均最慢 0.055 毫秒;正規表示式都是 .+ 接固定結尾,沒有巢狀重複 |
唯讀閘門 verify_jwt_in_request(optional=True) |
只有被判成寫入、而且開關打開時才解權杖 | 不會:解不開就 return None 交給後面,不做其他事 |
app_mw.py |
第 154 項本身 | (已報,不在本棒範圍) |
啟動檢查 config_loader.py |
讀環境變數的 JSON | 不接觸請求,只在啟動時跑一次 |
except Exception: return None,:201-204)時,閘門自己不擋,交給功能本身的 @jwt_required()。
jedi-common 1.2.0 的 session_scope 在沒有身分時設 is_super_admin='f',也不設可見公司路徑(db.py:212-215),所以查出來是空的,不是全部。| 疑點樣式 | 本棒結果 |
|---|---|
| 只驗「你是誰」沒驗「這筆是不是你的」 | 不適用:這一層只管「你是誰、切到哪家公司」;「切到哪家公司」有驗隸屬(見 ②) |
| 列表有守、單筆沒守 | 不適用 |
| route 裝飾器守門,但有第二支路由沒掛 | 🟡 類似的形狀成立一處:唯讀閘門只掛在 HTTP,即時通訊通道沒有對應的一道。填問卷時每改一題,就透過即時通訊的 on_update 事件寫進資料庫(jedi_survey/.../fill_survey_socketio_handler.py:164-191 → patch_task_survey_answer)。這條路不經過 before_request,身分套件的即時通訊驗證和問卷套件裡也都沒有任何唯讀或授權檢查(已 grep,零命中)。→ 併入 U2-3 |
守門條件用 or 串,其中一個永遠成立 |
❌ 不成立。唯讀閘門 if not enforcement_enabled() or not readonly_gate_enabled(): return None 是「任一開關關掉就不執法」,這是設計好的熄火保險(U1 已登記為資訊級) |
| 「查不到」和「沒權限」混成同一個回應 | 刻意分開:切到不隸屬的公司回 403、查不到使用者回 401 |
| 背景排程假設呼叫者一定是自己人 | 不適用本棒 |
| 防重放/計次只在單一進程有效 | 不適用本棒 |
| 註解寫「這裡刻意不檢查」但上一層沒檢查 | 🟡 成立一處:豁免名單註解說 /agents/ 是「機器對機器(mTLS),不屬使用者寫入行為」,但底下有兩支人操作的寫入 → U2-3 |
/static/,不必登入(中)core/app_factory.py:94-98app_factory.py、Dockerfile、docker-compose.yml、FE repo 的 nginx.onprem.conf;static_folder 的程式(問卷匯入),列了本機 static/ 的實際內容。static_folder 改成 None,或指向只放隨版唯讀資源的目錄。使用者上傳檔一律走有權限檢查的下載 API。core/app_factory.py:184(logger=True)、:186(engineio_logger=True);infra/support/collector/host_collector.py:136-156。useSurveySocket.js:57-58 auth: { token: accessToken }),所以權杖會原封不動出現在即時通訊容器的紀錄裡。guidant-socketio)打包帶走。mask_log_lines),容器紀錄卻沒有,直接把 docker logs 的輸出原樣放進包裡。python-socketio 5.16.4/python-engineio 4.14.0,照 app_factory.py 同樣的參數建一個伺服器,送一個帶假權杖的連線封包。SIDX: Received packet MESSAGE data 0/socket/fill-survey,{"token":"eyJhbGciOiJIUzI1NiJ9.FAKE.SIG"},權杖完整明文。engineio/base_server.py:70-75),而 Received packet 正是 INFO 等級。JWT_ACCESS_TOKEN_EXPIRES = 300000 秒)。docker logs --tail 有行數上限(CONTAINER_LOG_TAIL_LINES),所以只會帶到最近的一段。logger 參數改成 False(除錯時再用環境變數打開);mask_log_lines,並把 "token":"…" 這種 JSON 形狀加進遮罩鍵名。common/middleware/license_readonly_mw.py:76;jedi_survey/app/handler/fill_survey_socketio_handler.py:164-191/agents/ 放行「機器對機器」的請求,但 POST /agents/enroll-token(產生註冊碼)和 /agents/enroll-token/revoke(停用註冊碼)是管理員在畫面上按的,也被放行。_should_block 回 False。/users 放行、/users/ 就擋(實測)。這個方向是「多擋」不是「多放」,對讀取類不構成漏洞。/xxx,攻擊者送 /xxx/ 不會因此被放行,因為沒登記的一律當寫入擋。所以斜線這點實際上只會造成誤擋,不會造成漏放。/agents/ 改成逐支列出 register、heartbeat、tasks/,不要整個前綴放行;on_update)在寫入前呼叫 viewer_is_readonly(),是唯讀就回錯誤不寫。第一層:工具正式報告(經過三人面板投票)
verified:
failed 0。jwt_mw.py 追進身分套件、再對照出貨版本找到的。第二層:runner 自行開檔核對(未經三人面板投票)
jedi_iam.middleware 的 jwt_mw.py、core.py、context.py、socketio_auth.py;jedi_common 的 db.py;nginx.onprem.conf、useSurveySocket.js;Dockerfile、docker-compose.yml、install.sh。jedi-iam 1.3.0 與 jedi-common 1.2.0 的正式安裝檔(dist/*.whl),確認「作用公司隸屬檢查」與「超級管理員判斷」兩段有出貨、「停權檢查」沒有出貨。| 項目 | 值 |
|---|---|
| 掃描目標 | BE repo compliance-manager-be,branch feature/review |
| 版本 | 3f6290400(工作區有平行線未 commit 的改動,範圍 6 支檔案本身沒有改動) |
| 工具 | Claude Code 官方 claude-security plugin 0.11.0 |
| 參數 | mode scan/effort low/focus attack-surface/scope 6 檔 |
| run ID | wf_2ac3d75c-1b1 |
| 報告目錄 | CLAUDE-SECURITY-20260925-032606/(不入版控) |
| 耗時 | 約 82 分鐘(4,927 秒;研究員約 65 分鐘、密碼專掃約 13 分鐘、面板約 4 分鐘) |
| 研究員 | 2 派出/2 交回,failed 0 |
| 候選/面板票 | 5/15(4 成立、1 否決) |
| 驗證章 | verified |
| 工具發現 | 4 條(中 3、低 1),全部是總表既有項或 M13 第 17 條,不另計 |
| runner 自行發現 | 3 條(中 2、中低 1),都沒有經過面板 |
| 淨新增 | 中 2(U2-1、U2-2)+中低 1(U2-3),建議首腦決定是否開修正卡;U2-2 可與總表第 173 項(診斷包遮罩)併同一張卡 |