Stripe is Silently Recording Your Movements On its Customers' Websites

Michael Lynch

Stripe 正悄悄記錄你在其客戶網站上的瀏覽行為

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

  1. 使用者在我網站上造訪的每一個 URL,包含從未顯示 Stripe 付款表單的頁面
  2. 關於使用者在我網站上瀏覽時如何移動滑鼠游標的遙測資料
  3. 可用來將我網站的訪客與其他接受 Stripe 付款的網站進行關聯的唯一識別碼

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

誰會受到影響?

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

  • 你的基礎頁面範本包含了 Stripe 的 script 標籤:
    • <script src="https://js.stripe.com/v3">
  • 你的網站是 single-page app(單頁應用程式),例如使用 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 記錄的是完整 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 請求的那個網址:

https://m.stripe.com/4

我沒有在程式碼中找到錯誤,反而發現了數十篇其他開發者對此行為感到驚訝的貼文。這些回報可以一路追溯到 2017 年

關於此行為的先前使用者回報拼貼圖

多年來,開發者一直在 StackOverflow 和 GitHub 上發文討論 Stripe 的追蹤行為

在其中一個 GitHub 上的問題討論串中,一位 Stripe 員工表示此行為並非有意為之,Stripe 會尋求修正:

Adam Solove(亞當·索洛夫)在 GitHub 上的留言截圖

在一則 GitHub 留言中,Stripe 員工表示 Stripe.js 應該只在應用程式呼叫該程式庫時、且僅在使用者提交付款資訊的頁面上才發送資料。

那是 7 個月前的事了,從那之後我在該討論串或其他任何地方都沒有看到 Stripe 的後續回應。

確認問題

為了確認沒有其他程式碼觸發此行為,我建立了一個最小可重現專案。這是一個僅安裝了@stripe/stripe-js npm package 的陽春 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

如果你遇到任何其他問題或有其他疑慮,請告訴我們。

祝好,
費絲

「符合使用者最佳利益」這句話特別顯得居高臨下。從這次資料收集中獲益最多的顯然是 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 v.1.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 並未提供任何受支援的方式來卸載其程式庫或停用其使用者監控。有一位勇於嘗試的開發者建立了一個用來強行卸載所有 Stripe 程式碼的 JavaScript 程式碼片段,但該方法僅適用於 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 來將 Stripe 限制在你的付款頁面上。

以下是我應用程式中的實際呈現:

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

作品集再平衡工具使用逐頁 Content Security Policy 來防止 Stripe 在應用程式中除了付款頁面以外的任何地方載入。

要在單頁應用程式中實作逐頁的 Content Security Policy 有點棘手,因為預設情況下,當使用者導覽至新頁面時,瀏覽器不會向伺服器查詢新的政策。為了強制重新整理政策,我在付款頁面上使用 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 而實作 Content Security Policy,但如果你已經有可運作的政策,它就能提供額外的保障,確保 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 程式庫」,但柯里森指出此說法並不正確,因為網站有可能在不使用 Stripe JS 程式庫的情況下與 Stripe 整合。

更新:2020 年 4 月 30 日

Stripe 已修訂其隱私權政策與開發者文件,並在其 JavaScript 程式庫中加入讓整合者得以限制追蹤行為的功能。

我發表了一篇後續文章,討論 Stripe 的這些變更如何回應我所提出的疑慮:

原文由 Michael Lynch 發布

本文章由 muse-spark-1.2-contributor 進行翻譯