跳到主要内容

ArtinTech Solution

Stripe webhooks failing with 403 behind Cloudflare and the fix

那封邮件是在一个周日早上收到的。一家欧洲非营利基金会的运营总监转发过来,只附了一句话:请查看下方内容并处理。下面是 Stripe 的自动通知:几天来它一直无法把 Webhook 事件送达基金会的捐赠网站,已经失败了十九次;如果情况不变,一周内 Stripe 将彻底停止向这个 endpoint 发送通知。

对于一个通过 WordPress 接受单次和月度捐赠的机构来说,这绝不是小问题。Webhook 是网站得知一笔付款真正成功的唯一途径。没有它,捐赠会一直停留在待处理状态,捐赠者收不到收据,月度续费也不会被记录,而钱其实一直在进入 Stripe。

下面是我们排查这次故障的经过:为什么最先发现的问题确实存在却不是根源,以及最终那个没有关闭任何安全防护的小改动。

Stripe Webhook 的作用,以及失败为什么要紧

捐赠者付款时,浏览器会跳转到 Stripe 再返回。网站不能相信浏览器带回来的付款结果,所以 Stripe 还会从服务器直接调用网站,发送类似 payment_intent.succeeded 的事件。网站必须返回 200 到 299 之间的 HTTP 状态码,否则就算失败。Stripe 会以逐渐拉长的间隔重试,最长三天,之后放弃该事件。

这个网站使用的捐赠插件是 GiveWP,它在以 ?give-listener=stripe 结尾的地址上接收事件。网站是多语言的,因此有两个活跃的 endpoint,一个在 /de/ 下,一个在 /fr/ 下。两个都在失败,尽管 Stripe 的邮件只提到了其中一个。

Webhook 投递日志显示连续的 403 错误
从月初开始,每一次投递都返回 403。(示意图,信息已匿名化。)

第一个发现:问题真实存在,但不是原因

WordPress 后台一打开就显示激活错误:Recurring Donations 扩展被停用了,因为它需要比当前安装版本更新的 GiveWP。扩展已经自动更新,主插件却没有,于是 WordPress 悄悄停用了扩展。而且它的许可证被设置为不再续订,已经收不到更新。

这看起来是个完美的解释,而且无论如何都需要修复。我们先做了完整备份,更新 GiveWP 并重新启用扩展,结果网站前台立刻出现致命错误。错误堆栈指向一个弹窗,里面嵌入了一个早已删除的旧捐赠表单短代码。我们先停用扩展恢复网站,修正弹窗,再重新启用,并检查了首页、捐赠页和每一个语言版本。

网站恢复正常,按月捐赠的选项也回来了。可 Webhook 依然失败。

读懂真正的错误,而不是靠猜

停止猜测最快的方法,是在 Stripe 后台的 Developers → Webhooks 中打开一次失败的投递,看两样东西:状态码和响应正文。这里的状态码是 403 Forbidden,而响应正文根本不是 WordPress 的错误,而是一个标题为 Just a moment… 的 HTML 页面,加载着来自 challenges.cloudflare.com 的脚本。

失败的 Webhook 响应正文显示 Cloudflare 验证页面
响应正文暴露了真相:这是 CDN 返回的人机验证页面,而不是 WordPress。(示意图。)

这个页面就是 Cloudflare 的浏览器质询。它要求浏览器先证明自己是真人,才放行请求。用 Chrome 的人一秒钟就能通过,但 Stripe 的服务器不是浏览器,无法完成质询,所以每一个 Webhook 都在边缘节点被拦下。请求从未到达 WordPress,这也是网站日志里什么都看不到的原因。

找出是 Cloudflare 的哪项功能拦截了 Stripe

Cloudflare 有多项功能都可能质询流量,修复方式取决于具体是哪一项。我们在 Security → Analytics → Events 中按查询字符串 give-listener 过滤,每条匹配记录的动作都是 Managed Challenge,服务都是 Security level。

Cloudflare 安全事件显示 Webhook 请求被 Security Level 拦截
支付服务商的回调被当作普通访客一样质询。(示意图。)

同一个列表还揭示了另一件事:来自瑞士、法国和德国的普通访客在普通页面上也被质询。该区域的安全级别被调得很高,可能是为了应对之前的一次攻击,但没人注意到它把支付服务商也一起挡住了。

解决方法:一条精准的 Skip 规则,而不是削弱整站防护

降低安全级别当然也能解决,但那是为了一个 URL 改变整个网站的防护。更干净的做法是写一条自定义规则,只放行 Webhook 地址,其他一切保持不变。

  1. 进入 Security → Security rules,新建一条自定义规则。
  2. 取一个清晰的名字,例如 Allow Stripe webhooks。
  3. 匹配条件设为 URI Query String、contains、give-listener=stripe。
  4. 动作选择 Skip,并勾选 All remaining custom rules。
  5. 展开 More components to skip,勾选 Security Level 和 Browser Integrity Check。
  6. 将规则放在第一位并部署。
(http.request.uri.query contains "give-listener=stripe")
针对 give-listener=stripe 的 Cloudflare Skip 规则,已勾选 Security Level
规则只匹配 Webhook 接收地址。必须明确勾选 Security Level,因为正是它在拦截。(示意图。)

最容易忽略的是第五步。规则的第一个版本只跳过了 Browser Integrity Check,API 预览显示 "products": ["bic"],这样 Webhook 仍会失败。事件日志已经说明拦截来自 Security level,所以必须跳过的就是这一项。免费套餐还要注意:如果日志里显示的是 Bot Fight Mode,Skip 规则无法绕过它,只能直接关闭该设置。

这样安全吗?这条规则并不是盲目信任请求。GiveWP 仍会用 Stripe 的签名密钥验证每一个 Webhook,伪造的请求即使到达接收地址,也会被 WordPress 拒绝。验证 Stripe 的身份从来都不是 Cloudflare 的职责,而是签名的职责。

确认修复,并找回遗漏的捐赠

我们在 Stripe 后台重新发送了一个失败的事件,返回 200 OK。一小时内,Stripe 的自动重试也开始成功,后台将之前的失败标记为 Delivery recovered。第二天,Stripe 还发来邮件确认该 endpoint 已经恢复。

修复后 Webhook 投递返回 200 OK
规则生效后,投递返回 200 OK,Stripe 的重试清空了积压。(示意图。)

清理阶段有两点实用经验。第一,重新发送按钮只对每个事件最近一次的投递有效;对于 Stripe 已经放弃的事件,要打开事件本身再从那里重新发送。第二,要核对的是钱,而不只是 Webhook:把故障开始以来 Stripe 的每一笔付款与 WordPress 里的捐赠记录逐一比对。这次所有成功的付款都已记录在案,有一笔确实因余额不足而失败并被标记为失败,另外两笔排查期间产生的小额测试付款也被标注出来,避免被误算为捐赠。

给所有接受付款的 WordPress 网站的经验

  • 先看响应正文。包含 CDN HTML 的 403 不是程序缺陷,再怎么调试插件也修不好。
  • 安全设置和支付 Webhook 必须协调一致。遭受攻击后提高安全级别合情合理,但忘了它同样作用于服务器之间的回调,捐赠就会悄悄丢失。
  • 放行路径,而不是放行所有人。只针对接收地址的 Skip 规则能让网站其余部分继续受到保护。
  • 过期的扩展许可证是慢性风险。它会停止更新,下一次核心更新就可能毫无预警地停用扩展。
  • 关注告警。Stripe 会在停用 endpoint 前大约一周持续发出警告,务必确保这些邮件能送到有能力处理的人手里。

我们还写过一个类似的边缘案例,缓存与 WordPress 在时间上产生分歧:缓存 TTL 导致的间歇性 403 错误;以及能及早发现此类问题的例行检查:网站维护究竟包含什么。

常见问题

网站在我的浏览器里正常,为什么 Stripe Webhook 返回 403?

因为你的浏览器能通过 Cloudflare 的质询,而 Stripe 的服务器不能。如果失败响应的正文中出现 Just a moment 或 challenges.cloudflare.com,说明请求在到达 WordPress 之前就被 Cloudflare 拦下了。

是否应该改为把 Stripe 的 IP 地址加入白名单?

可以使用 Stripe 公布的 IP 列表,但列表会变化,需要持续维护。针对 Webhook 路径的 Skip 规则更简单,而且由于插件会验证签名,实际上同样安全。

Webhook 失败期间会丢失捐赠吗?

通常 Stripe 照常扣款,但网站不会记录。捐赠一直处于待处理状态,收据不会发出,定期续费也不会出现在记录中,直到事件成功送达或手动修正记录。

Stripe 会重试多久?

在正式模式下,最长重试三天,间隔逐渐拉长。如果 endpoint 持续失败,Stripe 会发邮件警告,最终将其停用。

你的 WordPress 网站在处理捐赠、电商或会员付款,并收到看不懂的支付告警?这正是我们 WordPress 开发和网站维护服务的日常工作。联系我们,我们会直接告诉你问题出在哪里、修复需要什么。

Share this article

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注