Skyvern TOTP 解析与占位符失败关闭机制otpauth URI 签发者不匹配修复方案详解【免费下载链接】skyvernAutomate browser based workflows with AI项目地址: https://gitcode.com/GitHub_Trending/sk/skyvern导读本文基于 Skyvern 开源仓库中的内部技术方案文档 docs/plans/2026-07-13-totp-uri-placeholder-fix.md深度解析 TOTP基于时间的一次性密码解析链路中的一个关键缺陷及其修复方案标准otpauth://TOTP URI 中标签label内签发者与issuer查询参数不一致时PyOTP 的严格校验会拒绝解析导致正常凭据无法生成验证码同时未解析完成的 TOTP 占位符可能被当作普通文本直接送入浏览器输入路径。读完本文你将掌握 Skyvern 中 TOTP 配置的解析与规范化流程、占位符失败关闭fail-closed机制的底层实现以及该修复方案的测试覆盖与验收标准。背景otpauth URI 的签发者冲突问题在双因素认证2FA场景中TOTP 密钥通常以两种形式存在裸 Base32 密钥如JBSWY3DPEHPK3PXP标准 otpauth URI如otpauth://totp/Example:userexample.com?secretJBSWY3DPEHPK3PXPissuerExample。otpauth URI 规范允许签发者issuer同时出现在两个位置标签label中形如otpauth://totp/LabelIssuer:userexample.com查询参数中形如?issuerQueryIssuer。规范要求二者一致但许多真实世界的身份验证器、企业门户或用户手工粘贴的 URI 并不严格遵循这一约定。当二者不一致时PyOTP 的pyotp.parse_uri()会抛出ValueError错误信息为 If issuer is specified in both label and parameters, it should be equal.。这导致 Skyvern 在解析这些合法凭据时直接失败用户无法完成登录。与此同时Skyvern 的凭据体系支持以占位符形式引用 TOTP 凭据——例如 1Password 的OP_TOTP、Bitwarden 的BW_TOTP、Azure Vault 的AZ_TOTP标记以及工作流中的placeholder_*_totp占位符。如果这类占位符在运行期未能被解析为真实验证码就可能被当作普通字符串传递到浏览器输入路径造成输入错误或安全风险。修复方案核心思路方案文档 docs/plans/2026-07-13-totp-uri-placeholder-fix.md 给出两个并行的修复目标签发者不匹配的可恢复保持严格的pyotp.parse_uri()校验为默认路径不变但允许仅因签发者不匹配这一特定错误进行一次仅校验的重试——在重试中剥离issuer查询元数据后再解析因为签发者信息本身不参与验证码生成占位符失败关闭任何输入路径DOM、可选项 selectable、CUA、自动补全 autocomplete在分支或输入前都必须先解析 TOTP 标记解析失败的占位符绝不进入浏览器输入方法而是抛出类型化错误NoTOTPSecretFound。实现细节credentials.py 的解析与重试逻辑常量与判空基础设施核心实现在 skyvern/forge/sdk/services/credentials.py。文件顶部定义了与修复直接相关的常量_ISSUER_MISMATCH_ERROR If issuer is specified in both label and parameters, it should be equal. _TOTP_PLACEHOLDER_PATTERN re.compile(rplaceholder_[A-Za-z0-9]_totp) _TOTP_MARKER_VALUES frozenset({OP_TOTP, BW_TOTP, AZ_TOTP})其中_ISSUER_MISMATCH_ERROR用于精确匹配 PyOTP 抛出的签发者不匹配错误只有该错误才触发重试路径_TOTP_PLACEHOLDER_PATTERN匹配工作流占位符如placeholder_my_totp_totp_TOTP_MARKER_VALUES是三种凭据供应商的 TOTP 标记1PasswordOP_TOTP见OnePasswordConstants、BitwardenBW_TOTP、Azure VaultAZ_TOTP见AzureVaultConstants。配套的判定函数is_unresolved_totp_placeholder(value)仅检测占位符模式is_unresolved_totp_value(value)同时检测占位符与三种供应商标记作为未解析 TOTP 值的统一判定入口。_parse_uri_totp签发者不匹配的受控重试_parse_uri_totp()是本次修复的核心函数credentials.py其流程如下def _parse_uri_totp(uri: str) - pyotp.TOTP | None: parsed_uri _safe_urlsplit(uri) if parsed_uri is None: LOG.warning(TOTP URI is malformed) return None query_pairs parse_qsl(parsed_uri.query, keep_blank_valuesTrue) issuer_count sum(key issuer for key, _value in query_pairs) if issuer_count 1: LOG.warning(TOTP URI contains duplicate issuer parameters) return None try: parsed_otp pyotp.parse_uri(uri) except ValueError as exc: if str(exc) ! _ISSUER_MISMATCH_ERROR: LOG.warning(Failed to parse TOTP config from URI, exception_typetype(exc).__name__) return None # 签发者元数据不影响验证码生成但 PyOTP 在校验配置前就拒绝冲突 if issuer_count ! 1: LOG.warning(TOTP URI is missing issuer metadata required for mismatch recovery) return None query_without_issuer urlencode([(key, value) for key, value in query_pairs if key ! issuer]) validation_uri urlunsplit(parsed_uri._replace(queryquery_without_issuer)) try: parsed_otp pyotp.parse_uri(validation_uri) except Exception as fallback_exc: LOG.warning(Failed to parse TOTP config from URI after ignoring mismatched issuer metadata, ...) return None ... if not isinstance(parsed_otp, pyotp.TOTP): LOG.warning(Parsed OTP URI is not a TOTP config) return None if parsed_otp.interval 0: LOG.warning(TOTP URI contains a nonpositive period) return None ...关键设计约束有三点只有签发者不匹配这一种错误才进入重试。任何其他ValueError非法 Base32、不支持的算法、重复issuer参数等都直接返回None保证严格校验不被绕过重试前必须恰好存在一个issuer查询参数。重复issuerissuer_count 1或缺省issuer元数据时即使报错文本匹配也不会触发恢复路径——因为此时无法安全剥离剥离issuer后仍走完整pyotp.parse_uri()校验。算法algorithm、位数digits、周期period、Base32 秘密、以及最终确认parsed_otp是pyotp.TOTP实例且interval 0等检查一项不落。对外三个解析函数的分工def parse_totp_secret(totp_secret: str) - str: # 返回规范化后的 Base32 秘密字符串无效时返回 def parse_totp_config(totp_secret: str) - pyotp.TOTP | None: # 返回可用的 TOTP 配置对象保留算法/位数/周期无效时返回 None def normalize_totp_config(totp_secret: str) - str: # 返回经过校验的配置字符串otpauth URI 保留全部参数去空白无效时返回 三者关系parse_totp_secret负责从任意输入裸密钥、URI、带分隔符/空白、URL 编码 URI中提取并校验 Base32 秘密parse_totp_config在 URI 场景下委托_parse_uri_totp保留完整配置在裸密钥场景下用pyotp.TOTP(parsed_secret)以默认参数构造normalize_totp_config则同时承担白名单校验与规范化输出职责——URI 参数被完整保留这正是修复签发者不匹配后算法、位数、周期不丢失的关键裸密钥则归一化去除空白和连字符。URL 编码输入的处理体现在三处parse_totp_secret中通过unquote生成候选 URI 列表逐一尝试parse_totp_config与normalize_totp_config同样先对无空白输入尝试unquote后再走 URI 分支。验证码生成def generate_totp_code(totp_secret: str, for_time: int | None None) - str: totp parse_totp_config(totp_secret) if not totp: raise ValueError(Invalid TOTP secret or otpauth URI) if for_time is None: return totp.now() return totp.at(for_time)for_time参数支持确定性测试指定 Unix 时间生成该时刻的验证码同时pyotp.TOTP.at()也被handler.py的多字段 TOTP 序列逻辑用于推进时间窗生成下一个验证码。实现细节handler.py 与 skyvern_page.py 的失败关闭类型化失败NoTOTPSecretFound所有未解析 TOTP 输入路径的失败出口是NoTOTPSecretFound定义于 skyvern/exceptions.py继承自SkyvernException错误信息为No TOTP secret found。在 skyvern/webeye/actions/handler.py 中该异常被统一包装为ActionFailure(NoTOTPSecretFound())返回且日志中不暴露 URI、密钥、占位符或生成的验证码。handler.py 的输入路径拦截在handler.py中所有浏览器输入入口都先经过统一判定DOM 文本路径_handle_text附近handler.py先判定is_unresolved_totp_placeholder(text_result)命中即返回ActionFailure(NoTOTPSecretFound())再判定值是否为供应商标记BW_TOTP/OP_TOTP/AZ_TOTP是则调用get_totp_secret_with_task解析真实密钥后生成验证码否则若is_unresolved_totp_value(text_result)仍为真同样直接失败关闭CUA 文本路径handler.py同样的占位符先行判定 标记解析 失败关闭顺序select / 可选项路径handler.pyis_totp_value时通过generate_totp_value_from_secret生成验证码NoTOTPSecretFound转为ActionFailure自动补全路径handler.pyTOTP 输入跳过自动补全逻辑仅走验证码生成分支粘贴路径handler.py对action.text中的内嵌占位符逐一判定is_unresolved_totp_placeholder(token_value)或解析后仍is_unresolved_totp_value一律失败关闭多字段 TOTP 序列_handle_multi_field_totp_sequencehandler.py首次输入生成并缓存验证码与时间窗后续位复用缓存缓存缺失、时间窗过期、验证码长度不足均返回ActionFailure(TOTPExpiredError())绝不会把未解析值送入输入框。skyvern_page.py 的生成脚本侧拦截在 skyvern/core/script_generations/skyvern_page.py 中生成脚本的输入方法统一收敛到_resolve_totp_placeholder_or_raise()skyvern_page.pyasync def _resolve_totp_placeholder_or_raise(self, value, totp_identifierNone, totp_urlNone) - str: if not is_unresolved_totp_value(value): return value try: resolved_value await self.get_actual_value(value, totp_identifiertotp_identifier, totp_urltotp_url) except NoTOTPSecretFound: raise except Exception as exc: raise NoTOTPSecretFound() from exc if is_unresolved_totp_value(resolved_value): raise NoTOTPSecretFound() return resolved_value该函数在fill、click、get_actual_value、单字段 TOTP、多字段get_totp_digit等入口处被调用如 skyvern_page.py、skyvern_page.py、skyvern_page.py。注意is_unresolved_totp_value同时匹配占位符与供应商标记因此BW_TOTP、OP_TOTP、AZ_TOTP这类标记值在生成脚本路径同样会被强制解析解析结果若仍包含标记则抛出NoTOTPSecretFound。测试覆盖test_parse_totp_secret.py方案要求新增的回归测试集中在 tests/unit/test_parse_totp_secret.py可以从源码中看到全部关键用例均已落地签发者不匹配的恢复修复的核心验收def test_otpauth_uri_with_mismatched_issuer_preserves_generation_config() - None: uri ( otpauth://totp/LabelIssuer:userexample.test ?secretJBSWY3DPEHPK3PXPissuerQueryIssueralgorithmSHA256digits8period60 ) expected pyotp.TOTP( JBSWY3DPEHPK3PXP, digesthashlib.sha256, digits8, interval60, ).at(0) assert parse_totp_secret(uri) JBSWY3DPEHPK3PXP assert normalize_totp_config(uri) uri # URI 参数被完整保留 assert generate_totp_code(uri, for_time0) expected # 算法/位数/周期不丢失该用例验证标签签发者LabelIssuer与查询签发者QueryIssuer不一致时秘密仍被正确提取parse_totp_secret、URI 配置原样保留normalize_totp_config、生成的验证码与手工构造的pyotp.TOTP(SHA256, 8 位, 60 秒周期)完全一致。URL 编码版本同样覆盖def test_url_encoded_otpauth_uri_with_mismatched_issuer_is_accepted() - None: ... encoded_uri quote(uri, safe) assert parse_totp_secret(encoded_uri) JBSWY3DPEHPK3PXP assert normalize_totp_config(encoded_uri) uri assert generate_totp_code(encoded_uri, for_time0) expected # SHA512, 7 位, 45 秒畸形配置必须保持拒绝防止重试被滥用参数化测试test_issuer_mismatch_does_not_bypass_invalid_config是本次修复的安全底线它确认签发者不匹配的恢复路径不会成为非法配置的逃生通道。下列情形即使issuer存在也必须全部返回空串拒绝查询参数组合拒绝原因issuerQueryIssuer无 secret缺少秘密secretNOT_VALID!issuerQueryIssuer非法 Base32secret...issuerQueryIssueralgorithmNOPE不支持的算法secret...issuerQueryIssuerdigits9不支持的位数secret...issuerQueryIssuerperiodinvalid畸形周期secret...issuerQueryIssuerperiod0/period-30非正周期secret...issuerQueryIssuerunknownvalue未知参数issuerFirstIssuerissuerSecondIssuer等重复 issuer重复签发者元数据iss%75erQueryIssuer编码变体重复重复签发者元数据配套测试还确认签发者一致matching issuer时非正周期period0、period-30与重复 issuer 同样被拒绝test_matching_issuer_uri_rejects_nonpositive_period、test_matching_issuer_uri_rejects_duplicate_issuerHOTP URI 不会被强制转为 TOTPtest_hotp_uri_is_not_coerced_into_totp畸形 otpauth authority如otpauth://[及其 URL 编码形式在三个解析函数中均安全返回而不抛异常test_malformed_otpauth_authority_is_rejected_without_raising。兼容性回归行为保持不变的既有用例空字符串返回空串test_empty_string_returns_empty裸 Base32 秘密原样返回带连字符JBSWY-Y3DP-EHPK-3PXP与带空白JBSWY3DP EHPK3PXP的输入被规范化为无分隔形式标准 otpauth URI标签/查询签发者一致正常解析支持默认参数之外的algorithmSHA256digits8period60非默认配置保留test_normalize_totp_config_preserves_otpauth_uri_params、test_generate_totp_code_uses_otpauth_uri_params未解析值检测codeOP_TOTP userresolved-secret这类内嵌供应商标记的字符串被is_unresolved_totp_value识别正则提取secret参数非 otpauth 链接如https://example.com?secret...仍有效。验收标准与实际落地情况对照方案文档 docs/plans/2026-07-13-totp-uri-placeholder-fix.md 定义的验收标准与仓库当前实现逐一对应验收标准落地证据不匹配签发者生成正确验证码且保留算法/位数/周期tests/unit/test_parse_totp_secret.py 两组用例 credentials.py 的重试路径签发者一致的 URI 与裸 Base32 密钥行为不变tests/unit/test_parse_totp_secret.py 兼容性用例秘密缺失/无效、算法/位数不支持、周期畸形、未知参数、HOTP URI 仍被拒绝tests/unit/test_parse_totp_secret.py 参数化拒绝用例无未解析的placeholder_*_totp值进入浏览器输入方法handler.py 等各路径的is_unresolved_totp_value拦截 skyvern_page.py 的_resolve_totp_placeholder_or_raise失败不暴露 URI、密钥、占位符或验证码NoTOTPSecretFoundexceptions.py只携带固定消息日志仅输出结构化警告字段从实现现状可以确认方案中新增解析器回归测试更新 credentials.py 的重试逻辑handler 与 skyvern_page 全路径解析前置三项实施步骤均已落地。方案中提及的 handler 测试与生成脚本测试证明浏览器输入方法在 DOM、selectable、CUA、autocomplete 路径上均不被调用同样与 handler.py 中每处ActionFailure(NoTOTPSecretFound())的失败关闭分支一一对应。验证与工程实践建议方案文档的 Verification 部分定义了标准的质量门禁可作为同类修复的工程实践参考TDD 驱动先用 tests/unit/test_parse_totp_secret.py 中新增的解析器与 handler 用例锁定行为再实现重试逻辑编译与静态检查对每个修改的 Python 文件执行编译与 lint回归套件运行更广范围的 credential/TOTP 回归套件与全部单元测试重点观察既有兼容性用例裸密钥、签发者一致的 URI、非默认参数是否出现行为漂移差异检查对变更文件执行 pre-commit 与git diff --check避免引入空白错误独立评审与 QA由独立的解析器/安全与动作失败评审人员复核随后单独执行一轮 QA 验证。在评估同类 TOTP 问题时以下边界条件值得优先检查URL 编码的 URI 输入%序列、重复issuer参数含编码变体iss%75er、非默认算法/位数/周期组合、以及占位符在粘贴与自动补全路径上的泄漏风险。小结本次修复在严格校验与兼容真实凭据之间找到了精确的平衡点签发者不匹配是唯一被允许剥离元数据重试的错误且重试后仍需通过 PyOTP 的完整配置校验与此同时所有浏览器输入路径以is_unresolved_totp_value为统一闸门未解析的占位符与供应商标记一律以类型化异常失败关闭。这一设计既修复了otpauth://URI 的兼容性问题又守住了秘密校验与输入安全的底线是值得借鉴的受控降级 失败关闭安全修复范式。【免费下载链接】skyvernAutomate browser based workflows with AI项目地址: https://gitcode.com/GitHub_Trending/sk/skyvern创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考