本卡屬 FR-114 資安修正(母卡 CM-2019),b4 總測回報收集卡 CM-2231 #35(V4 驗證 CM-2267 #95 抓到)與 #37(決策者 09-27 手測撞到)。兩件都在 jedi-iam 套件、都是小改,併一張。
件一(#37,中):登入時系統會產一組新的 email 驗證碼,而「產碼」與「重新發送」共用同一個 60 秒冷卻。結果同一帳號 60 秒內第二次登入直接被拒(HTTP 429『驗證碼寄送過於頻繁』),連第一關帳密都不讓過。最常撞的流程是「新帳號首次登入 → 強制改密碼 → 必須重登」:改完密碼馬上登,離上次登入不到 60 秒,必撞。190 log 今晚 11 次 429 全在 POST /login,其中 21:25:13 登入→21:25:15 改密碼成功→21:25:27 重登 429(retry_after=33s)。CM-1557 之後所有版本都有,不是 b4 回歸,但客戶第一天裝機就會撞。
件二(#35,高):AI 儀表板回應端已遮掉密碼雜湊等欄位,但 jedi-iam UserService.get_users() 的 INFO log 直接 f"Get users: {users}" 把整個 entity 清單 repr 進 log——190 api 容器 stdout 一行就是六個帳號的 password=(bcrypt 雜湊)、salt=、is_super_admin= 原值。同型寫法在套件內共八處(見在哪裡),roles 那處 STATE follow-up 早就記過「api log 印整個 RoleEntity 含 bcrypt hash」。拿得到 log 的人等於拿到全站密碼雜湊。
/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-iam(monorepo branch fix/security-b1,HEAD d94a472d,jedi-iam 版本 1.4.5)。只動 jedi_iam/ 子目錄、只 git add 該子目錄下的檔。/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/.claude/worktrees/wt-fix-security(branch fix/security-b1)。pyproject.toml:64 pin jedi-iam==1.4.5、:115 有註解掉的 path 行——改成上面工作區路徑取消註解,poetry update jedi-iam;path 改動不 commit。8005 已有舊 BE(1.21.0b2)在跑,手測另起 PORT=8006。件一
jedi_iam/mfa/app/service/email_service.py:80-106 gen_mfa_code()::88 cooldown_key、:90-92 有 ttl 就拋 429、:97-98 存新碼(300 秒)、:104-105 起 60 秒冷卻。docstring :83-86 明寫「登入首發與重送共用此路徑」——就是這句造成的
jedi_iam/mfa/app/service/email_service.py:108-120 send_otp()::120 呼叫 gen_mfa_code
jedi_iam/api/routes/otp_route.py:148-160 重新發送端點 OtpCodeResendRoute → :160 send_otp(這條才該吃冷卻)
宿主 core/plugins/identity.py:161(BE 工作區) 登入首發 dispatcher → send_otp(這條不該被冷卻擋登入)
jedi_iam/app/service/login_orchestrator.py:175 outcome.mfa_type = self._mfa.dispatch(user_uid)(首發入口)
件二(八處,全部改)
jedi_iam/app/service/user_service.py:98 logger.info(f"Get users: {users}") ← 190 抓到的就是這行
jedi_iam/app/service/user_service.py:414 logger.info(f"Get users: {users}")(menu)
jedi_iam/app/service/role_service.py:54 logger.info(f"Get roles: {roles}")
jedi_iam/app/service/role_service.py:108 logger.info(f"Get roles(menu): {roles}")
jedi_iam/app/service/tenant_service.py:54 / :141
jedi_iam/app/service/org_unit_service.py:56 / :121
jedi_iam/app/service/capability_service.py:55
jedi_iam/app/service/ui_route_service.py:51
gen_mfa_code() 拆成「首發」與「重送」兩種語意。建議:加參數 resend: bool = False(或拆兩支方法)。首發(resend=False):若 otp_code:{uid} 仍在有效期內就沿用舊碼、不重寄、不重起冷卻,直接回舊碼;舊碼過期才產新碼並起冷卻。重送(resend=True,只有 otp_route.py:160 傳):維持現行 60 秒冷卻。send_otp() 跟著帶參數;宿主 identity.py:161 不用改(預設首發)。先查再寫:沿用舊碼時 otp_fail 計數不要清(那是重送才清,見 :101)。docstring :83-86 那段改寫成新語意。len(users))或 uid 清單,不印 entity。不要只修 :98 一處——V4 抓到一處是因為只打了一支 API。順帶 git grep -n 'logger.info(f"' jedi_iam | grep -v "len(\|uid\|count" 掃一次 jedi-iam 內其他 f-string 帶 entity 的 log,有就一起改、回寫列出。件一屬登入核心,要寫。既有 tests/test_otp_verify_token_identity.py、tests/test_mfa_captcha_merge.py 看它們怎麼 mock RedisClient,同風格加 tests/test_otp_first_send_no_cooldown.py:① 首發兩次(間隔 <60s、舊碼未過期)第二次不拋、回同一碼;② 重送在冷卻內拋 OtpResendTooFrequentError;③ 首發但舊碼已過期 → 產新碼。突變:把首發改回走冷卻判斷,①要紅。件二用 caplog 斷言 get_users 的 log 不含 password=/salt=(突變:改回印 entity 要紅)。跑法 cd /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-iam && PYTHONPATH=. python -m pytest tests -q。
mfa_required:true(不是 429);redis otp_code:<id> 值不變。POST /otp-resend → 期待 429 MFA_429001 帶剩餘秒數(重送仍受節流)。GET /users、/users/menu、/roles、/tenants、/org-units 各一次,grep -c "password=\|salt=\|is_super_admin=" log/app.log 期待 0。