fix(auth): 摘除不可满足的 PasskeyFactor + 同步后清理 - #101
Merged
Merged
Conversation
同步带来的 __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 事件路径恢复)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
上游 v2 同步(#100)合并后的收尾,三项独立改动。其中一项是认证缺陷修复,建议优先看。
1.
fix(auth)摘除不可满足的PasskeyFactor(8acc755c)缺陷
PasskeyFactor是一个永远无法被满足的 MFA 因子:verify()无条件返回challenge_required,与提交内容无关——没有任何提交能让它satisfied/mfa/passkey/verify依赖get_current_active_user,需要已有 Token,而 MFA 阶段发生在铸 Token 之前mfa.py全文零 flow/FlowStore 引用——没有任何代码把验证结果回灌进登录流程三者叠加,passkey 作为 MFA 因子是一条断路。用真实
FlowService探针验证的后果:challenge=None,反复 advance 到max_attempts=10才失败 —— 登不进去/auth/flow的 passkey 无密码登录_after_credential算出 enrolled 因子必含 passkey(他刚用 passkey 登的)→mfa_required→ 自锁死循环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_factors的has_passkey形参、_builtin_factor_steps里的 lambda(顺带省掉每次登录一次PassKey.get_by_user_id查询)、/mfa/passkey/verify端点、零引用死代码_check_user_has_passkey。保留(passkey 仍是完整的主认证方式)
PassKey表与模型、/mfa/passkey/authenticate/{start,finish}、PasskeyLoginStep(step_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.advance的challenge_required分支覆盖),而不是删掉。新增 4 条回归测试锚定修复效果:
test_builtin_factors_expose_otp_only—— 内建因子集恒等于["otp"]test_passkey_only_user_password_login_succeeds_without_mfa—— 只绑 passkey 无 OTP,密码登录直接拿 Tokentest_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_extraction(9a2d3409)同步整合 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。