Skip to content

fix(captcha): 兼容新版 Flow 前端的 reCAPTCHA 取码 - #195

Open
liwei9745 wants to merge 1 commit into
TheSmallHanCat:mainfrom
liwei9745:fix/recaptcha-trusted-types
Open

liwei9745 wants to merge 1 commit into
TheSmallHanCat:mainfrom
liwei9745:fix/recaptcha-trusted-types

Conversation

@liwei9745

Copy link
Copy Markdown

问题现象

captcha_method = browser(有头浏览器打码)时,图片/视频生成固定在 38% 取码阶段失败:

生成失败: Failed to obtain reCAPTCHA token
耗时 365 秒(3 次重试 × ~2 分钟)

日志中反复出现:

[BrowserCaptcha] reCAPTCHA execute 未返回有效 token: reCAPTCHA placeholder element must be empty
[BrowserCaptcha] ... grecaptcha 未就绪,尝试补注入脚本:
  EvalError: Evaluating a string as JavaScript violates this document's Trusted Type assignment requirements.
[BrowserCaptcha] ... add_script_tag(https://www.recaptcha.net) 失败:
  TypeError: Failed to set the 'src' property on 'HTMLScriptElement':
  This document requires 'TrustedScriptURL' assignment.

同一账号/环境在 11/6 之前可以成功,11/8 起持续失败。

根因

Google Flow 前端在 11 月上旬灰度到新版本(响应 URL 的 bl 参数由
boq_labs-ai-sandbox-frontend_20261028.00_p0 变为
boq_labs-ai-sandbox-frontend_20261105.10_p0),同时命中旧代码的三个假设:

  1. 取码闭包捕获失效
    旧逻辑依赖页面调用 grecaptcha.enterprise.execute.bind(...),再由
    TRUSTED_RECAPTCHA_HOOK_SOURCE 把绑定后的函数存入
    window.__flow2apiTrustedRecaptchaExecute。新前端不再 bind,因此
    captured = false。

  2. 兜底渲染必然报错
    捕获不到闭包时,旧代码执行
    enterprise.render(document.documentElement, ...)。
    documentElement 永远有子节点,reCAPTCHA 会直接抛
    reCAPTCHA placeholder element must be empty,即日志中的报错。

  3. Trusted Types CSP 使两条注入兜底全部失效

    • page.wait_for_function("字符串表达式") 触发页面内字符串 eval,被
      Trusted Types 拒绝,就绪检测永远失败;
    • page.add_script_tag(...) 因 TrustedScriptURL 要求失败;
    • 页面内注入路径还存在 page.evaluate(js, arg1, arg2) 的多位置参数
      误用(Playwright Python 只接受一个 arg),实际从未执行成功。

验证

用真实账号 Cookie + 代理在本地 Playwright 中复现并确认:

captured: false                                  # 新前端不再 bind
direct execute(siteKey, {action}) -> ok          # token 长度 2660
render(fresh div) + execute(clientId) -> "only works with invisible reCAPTCHA"
render(document.documentElement)      -> "reCAPTCHA placeholder element must be empty"  # 与线上日志一致

应用本 PR 后,同一页面、同一 Cookie 下取码脚本返回:

{
  "ok": true,
  "debug": { "captured": false, "executeType": "function", "renderType": "function" },
  "token_len": 2660
}

改动内容

src/services/recaptcha_runtime.py

取码改为三级策略,逐级降级并在失败时返回每级的真实错误:

  1. 使用 hook 捕获的原始 execute 闭包(兼容旧前端,保留原有
    extension_hijack_detected 动作保护逻辑);
  2. 直接调用 grecaptcha.enterprise.execute.call(enterprise, siteKey, {action})
    ——企业版 v3 官方用法,无需 render,新前端实测可用;
  3. 最后才 render 到新建的空容器(不再使用 documentElement),
    再按 widget id 执行。

src/services/browser_captcha.py

  • 用 page.evaluate 轮询替换 page.wait_for_function,绕开 Trusted Types
    对字符串 eval 的限制;
  • 页面内注入改为 trustedTypes.createPolicy(...).createScriptURL(...),
    替换在 Trusted Types 下必然失败的 add_script_tag;
  • 修复 page.evaluate 多位置参数导致的 TypeError;
  • 自定义打码/分数测试路径复用同一套就绪检测与注入逻辑。

tests/test_recaptcha_runtime.py

新增回归测试:hook 未能捕获闭包时,必须走直接 execute 路径,并校验
this 绑定、site key 与 action 未被改写。

测试

python -m unittest tests.test_recaptcha_runtime tests.test_browser_captcha -v
Ran 19 tests ... OK

另外 tests/test_browser_captcha_personal.py 全部通过(recaptcha_runtime
同时被 personal 模式复用)。

兼容性

  • 旧前端行为不变:优先使用捕获到的原始闭包;
  • 新前端:直接 execute(siteKey);
  • captcha_method 为 personal 时共用同一份取码脚本,同样受益;
  • 未改动任何配置项或数据库结构。

新版 Flow 前端(boq_labs-ai-sandbox-frontend_20261105.10_p0 起)不再调用
grecaptcha.enterprise.execute.bind(),同时启用了 Trusted Types CSP,导致
有头浏览器打码(captcha_method=browser)在取码阶段持续失败,最终报
"Failed to obtain reCAPTCHA token"。

具体改动:

- 取码脚本新增直接执行 enterprise.execute(siteKey, {action}) 的路径。
  捕获闭包失败时不再渲染到 document.documentElement(该节点必然非空,
  会抛 "reCAPTCHA placeholder element must be empty"),改为渲染到
  新建的空容器兜底。
- 就绪检测改用 page.evaluate 轮询,替换 page.wait_for_function,避免
  Trusted Types 下字符串 eval 抛
  "violates Trusted Type assignment requirements"。
- 脚本注入改用 trustedTypes.createPolicy(...).createScriptURL(...),
  不再使用在 Trusted Types 下必然失败的 add_script_tag。
- 修复 page.evaluate 传入多个位置参数导致的 TypeError,此前页面内
  注入兜底路径从未真正执行。
- 同步修复自定义打码(分数测试)路径,并补充“新版前端不 bind”场景的
  回归测试。
terry-xu-2077 pushed a commit to terry-xu-2077/flow2api that referenced this pull request Nov 11, 2026
…ect execute before waiting for bind hook

Takes the PR's Trusted Types handling and structured errors, but reorders the
execute strategy: direct enterprise.execute.call first, fresh-container render
second, bind-hook capture (with a 10s wait) only as a last resort.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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