掃描日期:2026-09-05 掃描版本:jedi-python-package
feature/FR-075@67cb075工具:Claude Codeclaude-securityplugin v0.10.2.3(claude-security:scanworkflow) 狀態:✅ 已完整跑完並經驗證面板流程(verification.status: verified) 對應卡片:CM-1547(母卡 CM-1546) 現況:8 條發現全數已修,1.21.0 出貨——F1/F2/F6 → CM-1560(TLS 項退回重做一次)、F3/F5 → CM-1561(改裁關閉入口、程式碼保留)、F4 → CM-1562、F7 → CM-1563、F8 → CM-1564。早期卡 09-21 作廢,總表狀態見docs/security-report/M13-iam.md第 2~5、8、9 條
jedi-iam/jedi_iam/infra/adapter/(password / ldap / google 三個認證 adapter)+ jedi-iam/jedi_iam/app/service/login_service.py + jedi-iam/jedi_iam/app/service/user_auth_provider_service.py (外部身分綁定)+ jedi-iam/jedi_iam/domain/service/login_domain_service.pylow(single-researcher shape,非 medium 的元件矩陣)attack-surfaceresearchersDispatched: 2/researchersReturned: 2 (第一次失敗的嘗試不計入完成數)。另有一支必跑的 secrets 掃描 agent(因 focus 有設定),空手而回。8 條發現存活:5 條 HIGH、3 條 MEDIUM。
jedi-iam/jedi_iam/infra/adapter/authenticate_adapter/ldap/ldap_adapter.py:144 get_open_ldap_user_info()(AD 分支同構在 :185)/login 端點傳入的 username 未經任何跳脫直接內插進 LDAP search filter(f'(cn={username})')。*)改寫查詢範圍,配上下方 F2 的匿名 bind, 可匹配到目錄內任意條目、取得其 entryUUID,若該 entryUUID 有綁定紀錄即可無密碼登入該帳號。ldap3.utils.conv.escape_filter_chars 處理所有目錄輸入;另在輸入驗證層限制帳號可用字元集。@auth_required 保護、無其他跳脫層。:67 connect()is_ad=False(OpenLDAP 模式)時,Connection() 的 user/password 皆被強制設為 None, 等於做匿名 bind。使用者輸入的密碼從未真正送到目錄伺服器比對。jedi-iam/jedi_iam/infra/adapter/authenticate_adapter/google_auth/google_auth_adapter.py:54 authenticate()authenticate(username, password) 方法本體從未使用 password 參數,只憑 provider_user_id=username 查 user_auth_providers 有無綁定紀錄,查到即回傳該使用者、視為登入成功。 此路徑確實可達:auth_factory.py 無條件把 AuthenticateType.GOOGLE 映射到此 adapter, LoginService.process_login() 會依 provider='google' 派送過來。provider_user_id 的人, 帶任意密碼值即可登入該帳號。sub claim 取得,不可信任任何 client 端提供的字串。jedi-iam/jedi_iam/app/service/user_auth_provider_service.py:99 register_ad_user() (register_ldap_user、register_google_user 同構)register_ad_user/register_ldap_user/register_google_user 皆收攻擊者可自訂的 user_uid 參數,只檢查「目標使用者存在」與「該外部身分尚未被綁走」兩項,從未比對 caller 是否即為 user_uid 本人、也未檢查是否具備管理他人身分的 capability。get_user_context().login_name 雖有取用,但僅用來填 created_user/updated_user 稽核欄位。user_uid。user_uid 必須等於從 JWT 解出的 caller 本人,除非 caller 明確持有 「管理他人身分綁定」的 capability;此檢查要落在 app service 層,不能只靠 UI 不顯示入口。api/user_auth_provider/routes/user_auth_provider_route.py) 只掛 @jwt_required(),無 self-check(對照有做 self-check 的 /user-profile/<uid> 端點)。:142 register_google_user()register_google_user 把 request payload 裡的 third_party_password 原封不動存成 provider_user_id,沒有呼叫任何 Google API 做驗證。這個值之後就是 F3 登入時唯一比對的憑證。user_uid; 該 user_uid 尚無衝突的既有 Google 綁定。sub claim 取得。jedi-iam/jedi_iam/infra/adapter/authenticate_adapter/ldap/ldap_adapter.py:49 _get_ldap_server()ldap3.Server() 建構時未傳入 tls= 參數,ldap3 預設會建一個 validate=ssl.CERT_NONE 的 Tls() 物件,即使管理者設定了 SSL 或 STARTTLS 加密,憑證與 hostname 仍完全不驗。LDAP_ENCRYPTION 設為 SSL 或 STARTTLS(管理者試圖強化連線); 攻擊者具備應用與目錄伺服器之間的 MITM 網路位置。Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=...) 並傳給 Server(..., tls=tls); 無法驗證憑證時應 fail closed。jedi-iam/jedi_iam/infra/adapter/authenticate_adapter/password/password_adapter.py:27 authenticate()verify_user_exist_by_login_name 對不存在的 login name 拋 NotFound(AUTH_USER_NOT_FOUND) (HTTP 404),而密碼錯誤時 is_password_correct 拋 UnauthorizedError(LOGIN_AUTHENTICATION_FAILED) (HTTP 401)。兩者在密碼比對發生前就已可由 HTTP 狀態碼與 error_code 區分。/login 端點,無其他前提。change_password_route.py 的 forget_password_masks_missing_email), 但登入端點未套用等價保護,確認並非刻意設計而是遺漏。:87 test_connect()test_connect() 用 self.config.LDAP_USER/self.config.LDAP_SECRET(已存在系統設定裡的 服務帳號密碼)去 bind self.server;而 self.server 的來源(LDAP_SERVER)可由呼叫端在 connect-test 請求中指定——若請求省略 secret 但帶了 user,LdapService.init_config 會沿用已存的 secret,等於把production 服務帳號密碼送到呼叫端自訂的任意主機。ldap-config.create capability(比完整系統管理員窄的一項權限)的操作者, 可把連線測試目標指向自己控制的主機,攔截到本應只給合法目錄伺服器的服務帳號憑證, 用於橫向移動或憑證重用。ldap-config.create capability;系統設定內已有存好的 LDAP 服務帳號憑證。對照第一輪不完整掃描(母卡 CM-1546 提到、scan-findings-batch1.md 記錄)在這一塊找到過的: H-1(OpenLDAP 匿名 bind)、H-2/H-7(LDAP filter injection)、H-3(TLS 不驗憑證)、 H-4(Google 不驗憑證)、H-5(任何人可綁身分到他人帳號)、M-5(連線測試外洩服務帳號密碼)、 M-6(明文 bind)、M-9(帳號枚舉)。
本輪重新找到並經面板驗證的:H-1→F2、H-2/H-7→F1、H-3→F6、H-4→F3、H-5→F4(並額外拆出 F5 這條 更精確的子問題)、M-5→F8、M-9→F7。全部第一輪的舊發現在本輪都獲得重新確認,且這次多了驗證面板的 3-0 一致背書(第一輪僅靠人工開檔核對,未經工具面板)。
本輪沒有重新找到的:M-6(非 SSL/STARTTLS 時明文 bind)。核對範圍:connect() 方法(F2 所在函式) 本身確實只在 is_ad 且有 LDAP_ENCRYPTION 設定時才走加密路徑,若管理者把 LDAP_ENCRYPTION 設為 none,理論上仍會明文傳輸。這條在本次範圍內(ldap_adapter.py 也在掃描範圍),研究員與面板均未把它 單獨列為候選——推測是因為它的觸發前提(管理者主動關閉加密)與 F2(匿名 bind 完全不驗密碼)相比, 攻擊面被認為次要而未被獨立提出,而非掃描遺漏了這個檔案。此條列為待後續人工複核項目,不影響本輪 「已完整跑完並經驗證」的結論。
verification.status: verified