Stripe is Silently Recording Your Movements On its Customers' Websites

Michael Lynch

Stripe 正悄悄記錄你在其客戶網站上的一舉一動

原文由 Michael Lynch 發布,訂閱此部落格

在新創與科技公司之間,Stripe 似乎是處理金流時近乎一致的首選。當我需要為新的網頁應用程式加入付費訂閱功能時,Stripe 感覺就是最自然的選擇。然而整合完成後,我卻發現 Stripe 官方的 JavaScript 函式庫會記錄使用者在我網站上的所有瀏覽行為,並回報給 Stripe。這些資料包括:

  1. 使用者在我網站上造訪的每一個網址,包括那些從未顯示 Stripe 付款表單的頁面
  2. 使用者在瀏覽我網站時滑鼠游標移動的遙測資料
  3. 能讓 Stripe 將我網站的訪客與其他同樣透過 Stripe 收款的網站進行關聯的唯一識別碼

本文將分享我的發現、還有哪些人會受到影響,以及你如何在自己的網頁應用程式中限制 Stripe 的資料收集。

哪些人會受到影響?

如果你的網站符合以下任一情況,Stripe 就會在你的網站上收集這些資料:

  • 你的基礎頁面樣板中包含了 Stripe 的 script 標籤:
    • <script src="https://js.stripe.com/v3">
  • 你的網站是單頁應用程式,例如以 React、Vue 或 Angular 建立的網站,且你使用 Stripe 處理付款。

發現過程

我是在為我的投資組合再平衡工具加入付費方案時意外發現這件事的。在開發過程中,我使用了一個 HTTP 代理工具來檢視瀏覽器的 HTTP 流量。

在用 Stripe 成功實作應用程式的付款流程後,我注意到每一次頁面導覽都會產生一個新的 HTTP POST 請求,發送到 Stripe 的網址:

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 欄位會反映發出請求當下網址列中的網址。看起來 Stripe 正在記錄我應用程式中的每一次頁面瀏覽。更甚者,Stripe 記錄的是完整的網址,包含查詢參數與網址片段(例如 /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 請求的那個網址:

https://m.stripe.com/4

結果我沒有在自己的程式碼中找到錯誤,反而發現了數十篇其他開發者對於在自己應用程式中看到這種行為感到驚訝的貼文。這些回報最早可以追溯到 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 函式。只要客戶端應用程式 import 了這個函式庫,Stripe.js 就會立刻開始追蹤使用者行為。對於單頁應用程式來說,這發生在終端使用者載入網站任何一個頁面的瞬間。

注意loadStripe 是個容易誤導的名稱,因為 Stripe 在客戶端應用程式實際呼叫該函式之前就已經載入了。更貼切的名稱應該是 ensureStripeIsLoaded,因為這個函式的真正作用是在 Stripe 函式庫完成載入前,將應用程式的 API 呼叫排入佇列。

向 Stripe 回報此事

向 Stripe 支援團隊回報了這個問題,想確認這是否為預期行為,以及該如何防止它。

Stripe 很快就回覆我,表示使用者追蹤是刻意設計的,而且我實際上應該歡迎這項功能:

Hi 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 腳本

如前所述,只要應用程式 import 了函式庫,Stripe 就會立刻開始執行。有些開發者會刻意透過加入非同步包裝函式或使用程式碼分割來防止這種行為。

幸好,上週發布的 stripe-js v.1.4.0 版本提供了一個更乾淨的解法。這次更新引入了 @stripe/stripe-js/pure 這個 import 路徑,讓客戶端可以在沒有副作用的情況下 import 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 交易完成後就無法將其關閉。

(選用)以 Content Security Policy 縱深防禦

前述兩個步驟已足以防止 Stripe 的追蹤。若要進一步保護,可套用Content Security Policy(CSP)將 Stripe 限制在付款頁面上。

在我的應用程式中,設定看起來像這樣:

我的付款頁面與一般頁面 CSP 標頭的截圖

投資組合再平衡工具使用逐頁設定的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 互動或未顯示付款表單的頁面上也是如此。這些資料包括:

  • 使用者造訪的每個頁面的完整網址,包含查詢參數與網址片段
  • 使用者瀏覽時滑鼠移動速度的時間記錄
  • 一個讓 Stripe 能夠在網路上跨站追蹤同一使用者的 cookie

Stripe 並未清楚揭露其對這些資料的收集,且讓客戶端應用程式難以限制該函式庫的追蹤行為。

在非正式討論此問題時,Stripe 曾公開表示他們僅將這些資料用於詐欺防護與診斷,但其隱私權政策中的措辭卻暗示他們也可能將其用於行銷目的或出售。

給 Stripe 的建議

鑑於 Stripe 似乎非常重視安全與隱私,他們在如此缺乏透明度的情況下向客戶收集這麼多資料,令人驚訝。我希望這只是一個疏忽,之所以持續存在,只是因為注意到的人太少。

Stripe 可以採取以下幾項行動來改善此情況:

  • 清楚揭露資料分享。
    • npm 套件頁面與 Stripe.js 文件應清楚定義當客戶端應用程式整合該函式庫時,會將哪些瀏覽與遙測資料傳送給 Stripe。
  • 支援卸載函式庫。
    • 提供客戶端應用程式一個受支援的機制,在應用程式完成收款後卸載 Stripe。
  • 透過 opt-in 讓客戶端應用程式自行決定要分享哪些資料。
    • 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 進行翻譯

留言