Stripe is Silently Recording Your Movements On its Customers' Websites

Michael Lynch

Stripe 正在悄悄记录其客户网站上的用户行为

在初创企业和科技公司中,Stripe 似乎是几乎所有人都喜欢的支付处理服务商。当我需要为自己的新 Web 应用添加付费订阅功能时,Stripe 自然成了首选。然而,完成集成后我发现,Stripe 官方的 JavaScript 库会记录我网站上的所有浏览活动,并将其报告给 Stripe。这些数据包括:

  1. 用户在我网站上访问的每个 URL,包括那些从未显示 Stripe 支付表单的页面
  2. 用户浏览我网站时鼠标光标移动情况的遥测数据(telemetry)
  3. 能够让 Stripe 将我网站的访客与其他接受 Stripe 支付的网站上的访客关联起来的唯一标识符

本文将分享我的发现、还会影响哪些人,以及如何限制 Stripe 在你的 Web 应用中收集数据。

谁会受到影响?

如果以下任一情况成立,Stripe 就会在你的网站上收集这些数据:

  • 你的基础页面模板包含 Stripe 的脚本标签:
    • <script src="https://js.stripe.com/v3">
  • 你的网站是单页应用(single-page app),例如使用 React、Vue 或 Angular 创建的应用,并且你使用 Stripe 处理支付。

发现过程

我是在给自己的投资组合再平衡工具添加付费方案时偶然发现这一点的。在开发过程中,我使用了一个HTTP 代理,可以检查浏览器发出的 HTTP 流量。

成功使用 Stripe 实现应用的支付流程后,我注意到每次页面跳转都会向 Stripe 的某个 URL 生成一个新的 HTTP POST 请求:

每当我在应用中访问新页面时,Stripe.js 库都会向 Stripe 回报

这很奇怪,因为我访问的页面中没有任何调用 Stripe 库的代码。事实上,在用户创建账户之前,我的应用甚至不会收集支付信息;但当我以一个没有任何 Cookie 或存储凭据的新用户身份进入应用首页时,Stripe 仍然会发出 HTTP 请求。

Stripe 报告了什么?

Stripe 生成的所有外发请求都类似于下面这样:

POST /4 HTTP/1.1
Host: m.stripe.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: text/plain;charset=UTF-8
Content-Length: 692
Origin: https://m.stripe.network
Connection: close
Referer: https://m.stripe.network/inner.html
Cookie: m=e29f7c00-b748-4e5f-8625-34d14dbc1c01; m=e29f7c00-b748-4e5f-8625-34d14dbc1c01

JTdCJTIydjIlMjIlM0ExJTJDJTIyaWQlMjIlM0ElMjI4MTBiOWIxY2E3ODU5YzNlYzExYTY0NTI0NzNkMTZmYyUyMiUyQyUyMnQlMjIlM0E4JTJDJTIydGFnJTIyJTNBJTIyNC41LjIxJTIyJTJDJTIyc3JjJTIyJTNBJTIyanMlMjIlMkMlMjJhJTIyJTNBbnVsbCUyQyUyMmIlMjIlM0ElN0IlMjJhJTIyJTNBJTIyJTIyJTJDJTIyYiUyMiUzQSUyMmh0dHBzJTNBJTJGJTJGYXNzZXRyZWJhbGFuY2VyLmNvbSUyRnByaWNpbmclMjIlMkMlMjJjJTIyJTNBJTIyUG9ydGZvbGlvJTIwUmViYWxhbmNlciUyMiUyQyUyMmQlMjIlM0ElMjIxYjVhMDcxOS1jMTFjLTQwOTEtYWZiYi00NGE1MjRhMDM2ZGUlMjIlMkMlMjJlJTIyJTNBJTIyMWJhOTYwOWMtMjI0Ni00YjYwLTk1ZWUtYzg0YTRlNDhmOTkzJTIyJTJDJTIyZiUyMiUzQWZhbHNlJTJDJTIyZyUyMiUzQXRydWUlMkMlMjJoJTIyJTNBdHJ1ZSUyQyUyMmklMjIlM0ElNUIlMjJsb2NhdGlvbiUyMiU1RCUyQyUyMmolMjIlM0ElNUIlNUQlMkMlMjJuJTIyJTNBMTkzJTdEJTdE

底部显示的、以 JTdCJTIydj... 开头的字符串,是经过 URL 编码和 base64 编码的 JSON 数据。下面的 bash 命令可以将其解码为人类可读的字符串:

$ echo "JTdCJTIydjIlMjIlM0ExJTJDJTIyaWQlMjIlM0ElMjI4MTBiOWIxY2E3ODU5YzNlYzExYTY0NTI0NzNkMTZmYyUyMiUyQyUyMnQlMjIlM0E4JTJDJTIydGFnJTIyJTNBJTIyNC41LjIxJTIyJTJDJTIyc3JjJTIyJTNBJTIyanMlMjIlMkMlMjJhJTIyJTNBbnVsbCUyQyUyMmIlMjIlM0ElN0IlMjJhJTIyJTNBJTIyJTIyJTJDJTIyYiUyMiUzQSUyMmh0dHBzJTNBJTJGJTJGYXNzZXRyZWJhbGFuY2VyLmNvbSUyRnByaWNpbmclMjIlMkMlMjJjJTIyJTNBJTIyUG9ydGZvbGlvJTIwUmViYWxhbmNlciUyMiUyQyUyMmQlMjIlM0ElMjIxYjVhMDcxOS1jMTFjLTQwOTEtYWZiYi00NGE1MjRhMDM2ZGUlMjIlMkMlMjJlJTIyJTNBJTIyMWJhOTYwOWMtMjI0Ni00YjYwLTk1ZWUtYzg0YTRlNDhmOTkzJTIyJTJDJTIyZiUyMiUzQWZhbHNlJTJDJTIyZyUyMiUzQXRydWUlMkMlMjJoJTIyJTNBdHJ1ZSUyQyUyMmklMjIlM0ElNUIlMjJsb2NhdGlvbiUyMiU1RCUyQyUyMmolMjIlM0ElNUIlNUQlMkMlMjJuJTIyJTNBMTkzJTdEJTdE" \
  | base64 --decode \
  | python -c "import sys; import json; from urllib.parse import unquote; print(json.dumps(json.loads(unquote(sys.stdin.read())), indent=2, sort_keys=True))"
{
  "a": null,
  "b": {
    "a": "",
    "b": "https://assetrebalancer.com/pricing",
    "c": "Portfolio Rebalancer",
    "d": "1b5a0719-c11c-4091-afbb-44a524a036de",
    "e": "1ba9609c-2246-4b60-95ee-c84a4e48f993",
    "f": false,
    "g": true,
    "h": true,
    "i": ["location"],
    "j": [],
    "n": 193
  },
  "id": "810b9b1ca7859c3ec11a6452473d16fc",
  "src": "js",
  "t": 8,
  "tag": "4.5.21",
  "v2": 1
}

每当用户在我的应用中查看新页面时,Stripe 库都会生成一个这样的新请求。除了 URL 字段会反映请求发出时地址栏中的 URL 外,每个请求看起来都非常相似。看起来 Stripe 正在记录我应用中的每一次页面浏览。更严重的是,Stripe 记录的是完整 URL,包括查询参数和 URL 片段(例如 /account?id=12345#name=michael),而一些网站会利用这些内容存储敏感信息。

你可能已经从视频中注意到,最初加载应用时,第一个页面生成了两个请求,而其他每个页面加载只生成一个请求。下面是我解码第二个请求后发现的内容:

{
  "data": [4669, 20, 26, 13, 21, 20, 40, 21, 25, 14],
  "muid": "1b5a0719-c11c-4091-afbb-44a524a036de",
  "sid": "1ba9609c-2246-4b60-95ee-c84a4e48f993",
  "source": "mouse-timings-10",
  "url": "https://assetrebalancer.com/"
}

根据 mouse-timings 这个名称来看,Stripe 似乎正在记录用户的鼠标移动。

最后,每个请求都包含相同的 Cookie,用于唯一标识用户:

Cookie: m=e29f7c00-b748-4e5f-8625-34d14dbc1c01

这个 Cookie 允许 Stripe 在用户访问其他集成了 Stripe 的网站时跨网站跟踪他们,即使用户从未看到支付表单。

这是个错误吗?

起初,我以为这肯定是我的错误。一定是我在集成 Stripe 时粗心大意,才导致它错误地向服务器回传数据。

为了调查,我用 Google 搜索了接收我应用 HTTP POST 请求的 URL:

https://m.stripe.com/4

我没有发现自己代码中的 Bug,反而发现几十篇其他开发者发布的文章,他们也惊讶地看到自己的应用出现了这种行为。这些报告最早可以追溯到 2017 年

此前用户针对这一行为发布的报告拼图

多年来,开发者一直在 StackOverflow 和 GitHub 上发帖讨论 Stripe 的跟踪行为

在 GitHub 的一个 issue 讨论串中,一名 Stripe 员工表示这种行为并非有意为之,Stripe 会设法修复:

Adam Solove(亚当·索洛夫)在 GitHub 上评论的截图

在一条 GitHub 评论中,一名 Stripe 员工表示,Stripe.js 应只在应用调用该库时发送数据,而且只应在用户提交支付信息的页面发送。

那已经是 7 个月前的事了,此后我再也没在该讨论串或其他地方看到 Stripe 的任何后续回复。

确认问题

为了确定应用中没有其他东西触发这种行为,我创建了一个最小项目来复现该问题。这是一个极简 Vue 应用,只安装了 @stripe/stripe-js npm 包

我的实验确认,下面这行代码会导致 Stripe 库加载并开始跟踪用户:

import { loadStripe } from "@stripe/stripe-js";

请注意,我的应用甚至从未调用loadStripe 函数。只要客户端应用导入该库,Stripe.js 就会开始跟踪用户行为。对于单页应用来说,这会在最终用户加载网站任意页面的瞬间发生。

注意loadStripe 是一个具有误导性的名称,因为 Stripe 在客户端应用调用该函数之前就已经加载了。更合适的名称应该是 ensureStripeIsLoaded,因为该函数实际的作用是将应用的 API 调用排队,直到 Stripe 库完成加载。

向 Stripe 报告

向 Stripe 支持团队报告了这个问题,想确认这是否是预期行为,以及如何阻止它。

Stripe 很快回复了我,告知用户跟踪是有意设计的,而我实际上应该欢迎这一功能:

Michael(迈克尔),你好:

感谢你联系我们。我是 Stripe 支持团队的 Faith(费思)。

直入主题,你看到的这些调用是经过设计的,目的是检测欺诈行为,符合用户的最大利益。根据文档:“为了充分利用 Stripe 的高级欺诈功能,请在每个页面都包含此脚本,而不只是结账页面。这样,当客户浏览你的网站时,Stripe 就能检测可能表明存在欺诈的异常行为。”

https://stripe.com/docs/js/including

如果你遇到其他问题或有任何疑虑,请告知我们。

祝好,
Faith

“符合用户的最大利益”这句话尤其让人觉得居高临下。显然,从这类数据收集中获益最大的是 Stripe,而不是用户。Stripe 获得了用于训练其欺诈检测模型的免费数据,并且还有可能将这些信息出售给广告商。

对用户而言,Stripe.js 会迫使他们下载额外的 JavaScript 库,并从浏览器发送额外的 HTTP 请求,从而降低使用体验。即使用户从未访问过接受信用卡支付的页面,这种情况也会发生。

Stripe 是否披露了这一点?

我四处寻找 Stripe 关于这一行为的官方披露,但一无所获。我找到的最接近的内容,是他们的 npm 包说明中的这段含糊表述,Stripe 的支持代表也引用了它:

为了充分利用 Stripe 的高级欺诈功能,请确保 Stripe.js 在每个页面都已加载,而不只是结账页面。这样,当客户浏览你的网站时,Stripe 就能检测可能表明存在欺诈的异常行为。

隐私政策对其收集的数据说得更具体一些,但它暗示这些数据是在 stripe.com 上收集的,而不是在客户网站上收集的:

我们的网站使用 Cookie 和其他技术来有效运行。这些技术会记录你使用我们网站的相关信息,包括:

  • 浏览器和设备数据,例如 IP 地址、设备类型、操作系统和 Internet 浏览器类型、屏幕分辨率、操作系统名称和版本、设备制造商和型号、语言、插件、附加组件以及你所访问网站的语言版本;
  • 使用数据,例如在网站上停留的时间、访问的页面、点击的链接、语言偏好,以及将你引导或推荐到我们网站的页面。

我们还可能收集你一段时间内以及跨第三方网站、设备、应用和其他在线功能与服务的在线活动信息。

令人担忧的是,隐私政策还使用了宽泛的措辞,允许 Stripe 将这些数据出售给广告商:

当你访问我们的网站或在线服务时,我们和某些第三方都会收集你一段时间内以及跨不同网站的在线活动信息,以便向你提供根据个人兴趣定制的产品和服务广告(这种广告称为“基于兴趣的广告”)。

缓解措施

对于希望阻止 Stripe 进行这种侵入式跟踪的网站所有者来说,需要解决两个问题:

  1. 将 Stripe 库的执行延迟到用户进入需要支付的页面之后
  2. 用户完成支付后卸载 Stripe 库。

解决问题一:延迟 Stripe 脚本的加载

如上所述,应用一导入 Stripe 库,Stripe 就会开始执行。一些开发者会有意通过添加异步包装函数或使用代码分割来阻止这一行为。

幸运的是,上周发布的 stripe-js v.1.4.0 版本提供了更简洁的解决方案。此次更新引入了 @stripe/stripe-js/pure 导入路径,允许客户端在不产生副作用的情况下导入 Stripe:

import { loadStripe } from "@stripe/stripe-js/pure";

这样,Stripe 库的执行会延迟到应用明确调用 loadStripe 函数时。如果你将 loadStripe 函数的调用限制在涉及 Stripe 支付的页面或组件中,Stripe 就只会在这些页面加载,从而避免在浏览会话早期跟踪用户。

解决问题二:支付后卸载 Stripe

延迟加载 Stripe 库只是成功了一半。即使你只在支付时加载 Stripe,它的 JavaScript 仍会保留在应用中,并在用户会话剩余时间内继续跟踪用户。为防止这种情况,你的应用必须在客户完成支付后强制卸载 Stripe。

遗憾的是,Stripe 没有提供受支持的卸载其库或禁用用户监控的方法。一名勇敢的开发者创建了一段 JavaScript 代码片段,用于强制卸载所有 Stripe 代码,但它只适用于 React,而且由于依赖 Stripe 库中随时可能变化的未文档化属性,本质上是一种脆弱的解决方案。

我在自己的应用中通过用户离开支付页面时强制进行 HTTP 重新加载来解决这个问题。在 Vue 中,beforeRouteLeave 钩子会在离开页面之前执行。我添加了一个钩子来中断路由过程,并强制应用对下一个路由发起完整的 HTTP 请求:

beforeRouteLeave(to) {
  // Force an HTTP request instead of a JavaScript route change because we need
  // a new page load that does *not* import Stripe.
  window.location.replace(to.path);
}

(新增)禁用用户跟踪

添加日期:2020 年 4 月 30 日

我的文章最初发布后,Stripe 更新了其 JavaScript 库,加入了一个可以完全禁用跟踪的新参数。从 @stripe/stripe-js v1.5.0 起即可使用此选项:

import { loadStripe } from "@stripe/stripe-js/pure";

loadStripe.setLoadParameters({ advancedFraudSignals: false });
const stripe = await loadStripe("pk_test_TYooMQauvdEDq54NiTphI7jx");

这是一个受欢迎的选项,但缺点是该功能只有全有或全无两种状态。如果启用欺诈检测,除非强制重新加载页面(见上文),否则在 Stripe 交易完成后无法将其关闭。

(可选)使用 Content Security Policy(内容安全策略)进行纵深防御

前面两个步骤足以阻止 Stripe 的跟踪。为了获得额外保护,可以应用 Content Security Policy(CSP)将 Stripe 限制在支付页面中。

我的应用中的效果如下:

我的支付页面与普通页面上的 CSP 标头截图

Portfolio Rebalancer 使用逐页的 Content Security Policy,阻止 Stripe 在除支付页面之外的应用任何位置加载。

在单页应用中实现逐页 CSP 有点棘手,因为默认情况下,用户跳转到新页面时,浏览器不会向服务器请求新的策略。为了强制刷新策略,我在支付页面使用 Vue 的 beforeRouteEnter 守卫,在页面加载时强制发起新的 HTTP 请求。

beforeRouteEnter(to, from) {
  // If we're landing on this page from another route, force an HTTP request
  // so that we retrieve the route-specific Content Security Policy header.
  if (from.matched.length > 0) {
    window.location.replace(to.path);
  }
}

仅仅为了限制 Stripe 而实现 CSP 可能不值得,但如果你已经有可用的策略,它可以进一步确保 Stripe 库只在支付页面运行。

演示网站

为了在真实网站上查看 Stripe 的行为,我创建了一个展示这一行为的最小 Vue 应用:

总结

使用 Stripe 收款的网站通常会在其应用中包含 Stripe 的 JavaScript 库。按照 Stripe 文档进行集成,会导致该库在用户整个浏览会话期间将跟踪数据分享给 Stripe,即使用户访问的页面没有与 Stripe 交互,也没有显示支付表单。这些数据包括:

  • 用户访问的每个页面的完整 URL,包括查询参数和 URL 片段
  • 用户浏览期间鼠标移动速度的计时数据
  • 允许 Stripe 在整个 Web 上跟踪同一用户的 Cookie

Stripe 没有明确披露其对这些数据的收集,而且还让客户端应用难以限制该库的跟踪行为。

在非正式讨论这一问题时,Stripe 曾公开表示,他们只将这些数据用于欺诈防护和诊断;但其隐私政策中的措辞表明,他们也可能将这些数据用于营销或出售这些数据。

给 Stripe 的建议

鉴于 Stripe 似乎非常重视安全和隐私,他们在缺乏透明度的情况下从客户那里收集如此多的数据,实在令人惊讶。我希望这只是一个疏漏,因为注意到这一点的客户太少,所以一直没有得到解决。

Stripe 可以采取以下几项措施来纠正这一情况:

  • 明确披露数据分享情况。
    • npm 包页面和 Stripe.js 文档应明确说明:客户端应用集成该库后,库会向 Stripe 传输哪些浏览数据和遥测数据。
  • 支持卸载库。
    • 为客户端应用提供受支持的机制,以便在应用收集到用户支付信息后卸载 Stripe。
  • 通过选择加入机制,让客户端应用控制要分享的数据。
    • Stripe 客户需要承担针对其应用发起的拒付成本,因此他们应该能够决定与 Stripe 分享多少信息,以减少这些拒付。

更新:2020 年 4 月 21 日——美国东部时间下午 2:47

Stripe 联合创始人 Patrick Collison(帕特里克·科里森)回应了本文,再次确认 Stripe 承诺仅将收集的数据用于欺诈检测。他补充说,Stripe 很快会澄清服务条款中有关其数据收集做法的表述。

更正:文章最初写道:“使用 Stripe 收款的网站必须包含 Stripe 的 JavaScript 库。”但 Collison 指出这并不准确,因为网站无需使用 Stripe JS 库也可以与 Stripe 集成。

更新:2020 年 4 月 30 日

Stripe 已修订其隐私政策和开发者文档,并在其 JavaScript 库中添加了让集成方能够限制跟踪行为的功能。

我发布了一篇后续文章,讨论 Stripe 的改动如何回应我提出的担忧:

原文由 Michael Lynch 发布

本文章由 openai/gpt-5.6-luna 进行翻译