Stripe is Silently Recording Your Movements On its Customers' Websites

Michael Lynch

Stripe 正在其客户网站上悄悄记录你的浏览行为

原文由 Michael Lynch 发布,订阅该博客

在创业公司和科技公司中,Stripe 似乎是处理支付时近乎一致的首选。当我需要为新开发的网页应用添加付费订阅功能时,Stripe 自然成了我的第一选择。然而接入之后我发现,Stripe 官方的 JavaScript 库会记录用户在我网站上的所有浏览行为,并回传给 Stripe。这些数据包括:

  1. 用户在我网站上访问的每一个 URL,包括那些从未展示过 Stripe 支付表单的页面
  2. 用户在浏览我网站时鼠标移动轨迹的遥测数据
  3. 能够让 Stripe 将我网站的访客与其他同样接入 Stripe 支付的网站进行关联的唯一标识符

本文将分享我的发现、受影响的范围,以及如何在你的网页应用中限制 Stripe 的数据收集。

哪些人会受到影响?

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

  • 你的基础页面模板中包含了 Stripe 的 script 标签:
    • <script src="https://js.stripe.com/v3">
  • 你的网站是单页应用,比如用 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 时犯了某个粗心的错误,才导致它错误地向外回传数据。

为了调查,我在谷歌上搜索了接收我应用中 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 都没有再跟进。

确认问题

为了确认不是我应用中的其他东西触发了这一行为,我创建了一个最小化项目来复现问题。这是一个仅安装了@stripe/stripe-js npm 包的极简 Vue 应用。

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

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

请注意,我的应用甚至从未调用loadStripe 函数。Stripe.js 在客户端应用导入该库后就会立即开始追踪用户行为。对于单页应用来说,这发生在终端用户加载网站任意页面的那一刻。

loadStripe 是一个容易误导的名称,因为在客户端应用调用该函数之前,Stripe 就已经加载了。更合适的名字应该是 ensureStripeIsLoaded,因为该函数的实际作用是在 Stripe 库完成加载前,将应用的所有 API 调用排队等待。

向 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 地址、设备类型、操作系统和互联网浏览器类型、屏幕分辨率、操作系统名称和版本、设备制造商和型号、语言、插件、附加组件以及你所访问网站的语言版本;
  • 使用数据,例如在网站上花费的时间、访问的页面、点击的链接、语言偏好,以及引导或引荐你访问我们网站的页面。

我们还可能在一段时间内,在不同网站、设备、应用及其他在线功能和服务中,收集关于你在网站和关联设备上的在线活动的信息。

更令人担忧的是,隐私政策中还有措辞宽松的条款,允许 Stripe 将这些数据出售给广告商:

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

缓解措施

要阻止 Stripe 这种侵入性追踪,网站所有者需要解决两个问题:

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

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

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

幸运的是,上周发布的 stripe-js v1.4.0 版本提供了一个更干净的解决方案。该更新引入了 @stripe/stripe-js/pure 导入路径,允许客户端在无副作用的情况下导入 Stripe:

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

这会将 Stripe 库的执行推迟到应用显式调用 loadStripe 函数时。如果仅在涉及 Stripe 支付的页面或组件中调用 loadStripe 函数,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 交易完成后就无法将其关闭,除非强制刷新页面(见上文)。

(可选)通过内容安全策略实现纵深防御

前两步已足以阻止 Stripe 的追踪。如需额外保护,可以应用内容安全策略(CSP),将 Stripe 限制在你的支付页面上。

在我的应用中,它看起来是这样的:

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

Portfolio Rebalancer 使用按页面配置的内容安全策略来防止 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 在全网追踪同一用户的 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 的这些改动如何回应了我提出的担忧:

本文章由 muse-spark-1.2-contributor 进行翻译

评论