Skip to content

fix(auth): 摘除不可满足的 PasskeyFactor + 同步后清理 - #101

Merged
Aqr-K merged 3 commits into
v3-pythonfrom
chore/post-sync-cleanup
Aug 8, 2026
Merged

fix(auth): 摘除不可满足的 PasskeyFactor + 同步后清理#101
Aqr-K merged 3 commits into
v3-pythonfrom
chore/post-sync-cleanup

Conversation

@Aqr-K

@Aqr-K Aqr-K commented Aug 8, 2026

Copy link
Copy Markdown
Owner

上游 v2 同步(#100)合并后的收尾,三项独立改动。其中一项是认证缺陷修复,建议优先看。

1. fix(auth) 摘除不可满足的 PasskeyFactor8acc755c

缺陷

PasskeyFactor 是一个永远无法被满足的 MFA 因子:

  • verify() 无条件返回 challenge_required,与提交内容无关——没有任何提交能让它 satisfied
  • 设计上负责"真正验证"的 /mfa/passkey/verify 依赖 get_current_active_user需要已有 Token,而 MFA 阶段发生在铸 Token 之前
  • mfa.py 全文 flow/FlowStore 引用——没有任何代码把验证结果回灌进登录流程

三者叠加,passkey 作为 MFA 因子是一条断路。用真实 FlowService 探针验证的后果:

场景 实际行为
只绑 passkey、无 OTP 的用户密码登录 卡在 challenge=None,反复 advance 到 max_attempts=10 才失败 —— 登不进去
/auth/flow 的 passkey 无密码登录 断言校验成功后,_after_credential 算出 enrolled 因子必含 passkey(他刚用 passkey 登的)→ mfa_required自锁死循环
启用 passkey 的用户输错 OTP failure 降级为 challenge,导致 emit_auth_event 不发 AuthFailed风控/审计插件对爆破尝试失明(限流器仍在,属检测盲区非无限猜测)

处置

跟进上游 4b9af5b8。上游摘除 passkey-as-MFA 的理由是账号锁死——RP-ID/origin 配置一变 passkey 即不可验证,而它当时是强制门禁;上游先加 PASSKEY_ALLOW_REGISTER_WITHOUT_OTP 打补丁,最终在 jxxghp#6178 直接拆耦合。

本仓其实已吸收该策略变更的大部分mfa_status 只看 OTP、otp_disable 无 passkey 阻断、注册无 OTP 前置),残留的只有这个空壳。

删除:PasskeyFactor 类、build_builtin_factorshas_passkey 形参、_builtin_factor_steps 里的 lambda(顺带省掉每次登录一次 PassKey.get_by_user_id 查询)、/mfa/passkey/verify 端点、零引用死代码 _check_user_has_passkey

保留(passkey 仍是完整的主认证方式)

PassKey 表与模型、/mfa/passkey/authenticate/{start,finish}PasskeyLoginStepstep_id="system:passkey")、全部注册/管理端点。已启用 PASSKEY_REQUIRE_UV,passkey 本身即「持有 + UV」两因子。

净效果是恢复功能而非削减:用户的 passkey 依然能登录,只是不再被要求「验证 passkey 之后再验证一次 passkey」。数据层零影响,无迁移,无凭证失效。

测试处理

test_wrong_otp_with_both_enrolled_falls_to_passkey_challenge 断言的是引擎的通用 AnyOf 回落契约("另有带外因子时 OTP deny 不硬失败")——这条契约对插件带外因子仍然成立,所以改用文件内 _OutOfBandFactor 桩保留覆盖(同时保住 FactorStep.advancechallenge_required 分支覆盖),而不是删掉。

新增 4 条回归测试锚定修复效果:

  • test_builtin_factors_expose_otp_only —— 内建因子集恒等于 ["otp"]
  • test_passkey_only_user_password_login_succeeds_without_mfa —— 只绑 passkey 无 OTP,密码登录直接拿 Token
  • test_passkey_primary_login_issues_token_without_mfa —— PasskeyLoginStep 端到端 begin→challenge→advance→Token,不再自锁
  • test_wrong_otp_fails_hard_for_user_with_passkeys —— 输错 OTP 返回 failure,审计事件路径恢复

⚠️ 前端需配合

/api/v1/mfa/passkey/verify 契约消失;factors_available 不再出现 "passkey"。上游同一 commit 也删了该端点,推测其前端已不调用,但 v3 前端若已分叉需自行确认

2. chore(plugin) 删除仅测试可达的 plan_release_zip_extraction9a2d3409

同步整合 PR #97 时的遗留:该函数自合入起从未被生产代码调用,运行路径走的是同步带来的 __iter_release_zip_targets——它是严格超集(同样整体规划 + 越界整体拒绝,额外拒符号链接/设备文件、反斜杠、Windows 盘符、UNC,且已改用 SystemUtils.is_within() 落实 #97 的信任边界统一)。

逐条核对 TestPlanReleaseZipExtraction 的 4 个用例,全部已被 test_plugin_helper.py 针对运行路径的测试覆盖且更严格(绝对路径校验发生在前缀剥离之前,拦截更早)。Zip Slip 安全覆盖无净减少。

3. docs(comment) 清理叙事型注释(6ca6b665

本仓禁止重构过程叙事型注释。5 处「迁入」「⑦a 分解出的」等表述改写为直接描述现状,保留其中的功能信息TransferResultProcessor 的职责边界、handle 契约、name-mangled 方法关系),只去掉「代码是怎么变成现在这样」的过程记录。经 git blame 确认是 2026-06-18 本仓自身重构遗留,非上游同步引入。

测试

3178 passed / 1 skipped / 0 failed(+18 subtests)。

基线 3178 → 删除 4 个重复用例 → 3174 → 新增 4 条回归 → 3178。认证全套 83 passed。

Aqr-K added 3 commits August 8, 2026 03:06
同步带来的 __iter_release_zip_targets 已是 plan_release_zip_extraction 的超集:
同样做写盘前整体规划、越界整体拒绝,并额外拒绝符号链接/设备文件、反斜杠、
Windows 盘符、UNC 路径,是运行路径上唯一实际调用的实现(同步/异步解压站点
各一次)。plan_release_zip_extraction 自 PR #97 合入后从未被生产代码调用,
仅 tests/test_path_trust_boundary.py 引用。

删除该函数及其测试类 TestPlanReleaseZipExtraction。Zip Slip 相关安全覆盖
不减少:tests/test_plugin_helper.py 中的
test_install_from_release_rejects_unsafe_zip_member(覆盖 ../ 遍历、绝对
路径、反斜杠、Windows 盘符、UNC 路径、符号链接)、
test_install_from_release_extracts_zip_with_top_level_directory(正常场景
剥离公共前缀)、test_install_from_release_reports_empty_zip /
test_install_from_release_reports_directory_only_zip(空包/无可写文件)
已针对 __iter_release_zip_targets 覆盖了原测试类的全部场景,且更严格
(额外的名称校验先于前缀剥离执行,绝对路径条目在任何情况下都会被拒绝)。
app/chain/transfer.py 的 TransferResultProcessor 类 docstring 与
tests/test_transfer_default_callback_decompose.py 的模块 docstring/内联注释
含 5 处「迁入」「⑦a 分解出的」等重构过程记录,违反项目注释规范(只写功能
描述,不写代码是怎么变成现在这样的)。改写为直接描述当前职责/契约,功能
信息(TransferResultProcessor 的职责边界、暴露的方法、与 chain 的组合关系、
哪些方法是 name-mangled 的)保留不变。

grep -rn "迁入\|re-export 保留\|先桥后删\|⑦" app/ tests/ --include=*.py 已清零。
PasskeyFactor.verify 无条件返回 challenge_required,与提交内容无关,没有任何
提交能让它 satisfied;设计上承担"真正验证"的 /mfa/passkey/verify 依赖
get_current_active_user(需已有 Token),而 MFA 阶段发生在铸 Token 之前,
mfa.py 亦无任何 flow/FlowStore 回灌路径。该因子因此是不可满足的空壳。

实际后果:
1. 只绑 passkey、未开 OTP 的用户密码登录卡在 challenge,反复 advance 至
   max_attempts 才失败,无法登入;
2. 经 /auth/flow 用 passkey 主认证登录成功后,_after_credential 仍把 passkey
   算作已注册因子 → mfa_required 自锁;
3. 启用 passkey 的用户输错 OTP 由 failure 降级为 challenge,emit_auth_event
   不再发 AuthFailed,风控/审计插件看不见暴破尝试。

改动:
- 删 PasskeyFactor 类,build_builtin_factors 去掉 has_passkey 形参
- auth._builtin_factor_steps 去掉 has_passkey lambda(顺带省掉每次登录一次
  PassKey.get_by_user_id 查询)
- 删 /mfa/passkey/verify 端点与零引用的 _check_user_has_passkey

passkey 作为主认证方式全部保留:PassKey 表与模型、PasskeyLoginStep
(system:passkey)、/mfa/passkey/authenticate/{start,finish}、注册与管理端点,
数据层零影响、无迁移。

新增回归测试锚定:内建因子集只含 otp;只绑 passkey 的用户密码登录直接成功;
passkey 主认证直接产出 Token 不再要 MFA;已绑 passkey 的用户输错 OTP 返回
failure(AuthFailed 事件路径恢复)。
@Aqr-K
Aqr-K merged commit a652149 into v3-python Aug 8, 2026
1 check failed
@Aqr-K
Aqr-K deleted the chore/post-sync-cleanup branch August 8, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant