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のスクリプトタグが含まれている場合
    • <script src="https://js.stripe.com/v3">
  • ウェブサイトがReact、Vue、Angularなどで作られたシングルページアプリケーションであり、決済処理にStripeを使っている場合

発覚の経緯

有料プランをportfolio rebalancerに追加しているときに、偶然この挙動に気づきました。開発の一環として、ブラウザからの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)を記録しており、これらは一部のウェブサイトでは機密情報の保存に使われています。

動画から気づいた方もいるかもしれませんが、アプリを最初に読み込んだとき、最初のページだけ2つのリクエストを生成し、それ以外のページ読み込みでは1つだけでした。その2つ目のリクエストをデコードすると、次のような内容でした。

{
  "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の統合で不注意なミスを犯し、誤って外部に通信してしまっているに違いないと考えたのです。

調査のため、アプリからHTTP POSTリクエストを受信していたURLを検索してみました。

https://m.stripe.com/4

自分のコードのバグを見つけるどころか、この挙動に驚いた他の開発者による投稿を何十件も見つけました。報告は2017年まで遡ります

この挙動に関する過去のユーザー報告のコラージュ

開発者たちは何年にもわたって、Stack OverflowやGitHubでStripeのトラッキング挙動について投稿してきました

GitHubのあるイシュースレッドでは、Stripeの従業員がこの挙動は意図したものではないとし、修正を検討すると述べていました

GitHub上のAdam Solove氏のコメントのスクリーンショット

GitHubのコメントで、Stripeの従業員はStripe.jsはアプリがライブラリを呼び出したとき、かつユーザーが決済情報を送信するページでのみデータを送信すべきだと示唆している

それは7か月前のことでしたが、そのスレッドでも、私が見つけられた他のどこでも、Stripeからの続報はありませんでした。

問題の再現

私のアプリの他の何かがこの挙動を引き起こしているのではないかと確かめるため、この問題を再現する最小構成のプロジェクトを作成しました。最小限のVueアプリで、@stripe/stripe-js npmパッケージだけをインストールしたものです。

実験の結果、次の1行で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による公式な開示を探してみましたが、何も見つかりませんでした。最も近いものは、Stripeサポートの担当者が引用していた、npmパッケージの説明にある次の曖昧な一文でした。

Stripeの高度な不正検出機能を最大限に活用するには、Stripe.jsがチェックアウトページだけでなく、すべてのページで読み込まれるようにしてください。これにより、顧客がウェブサイトを閲覧する際の不正を示唆する可能性のある異常な行動をStripeが検出できるようになります。

プライバシーポリシーには収集するデータについて少しだけ具体的に書かれていますが、顧客のサイトではなくstripe.com上でデータを収集しているかのような印象を与えます。

当社のサイトは、効果的に機能するためにCookieやその他の技術を使用しています。これらの技術は、お客様による当社サイトの利用に関する情報を記録します。これには以下が含まれます。

  • ブラウザおよびデバイスデータ(IPアドレス、デバイスの種類、OSやインターネットブラウザの種類、画面解像度、OSの名前とバージョン、デバイスのメーカーやモデル、言語、プラグイン、アドオン、アクセスしているサイトの言語バージョンなど)
  • 利用状況データ(サイトでの滞在時間、訪問したページ、クリックしたリンク、言語設定、当社サイトにたどり着くきっかけとなったページなど)

当社はまた、ウェブサイトや接続されたデバイス上でのお客様のオンライン活動に関する情報を、時間の経過とともに、またサードパーティのウェブサイト、デバイス、アプリ、その他のオンライン機能やサービスを横断して収集する場合があります。

さらに懸念されるのは、プライバシーポリシーに、このデータを広告主に販売することを可能にする曖昧な文言が含まれていることです。

お客様が当社のサイトやオンラインサービスにアクセスする際、当社および特定の第三者は、お客様の個々の興味に合わせた商品やサービスに関する広告を提供するために、時間の経過とともに、また異なるサイトを横断して、お客様のオンライン活動に関する情報を収集します(この種の広告は「インタレストベース広告」と呼ばれます)。

対策

サイト所有者がStripeによるこの侵入的なトラッキングを防ぐには、2つの問題を解決する必要があります。

  1. ユーザーが決済が必要なページに到達するまで、Stripeライブラリの実行を遅延させること
  2. ユーザーが決済を完了した後に、Stripeライブラリをアンロードすること

問題1の解決:Stripeスクリプトの読み込みを遅延させる

前述のとおり、Stripeはアプリがライブラリをインポートしただけで実行を開始します。一部の開発者は、非同期のラッパー関数を追加することやコード分割を使うことで、意図的にこの挙動を防いでいます。

幸いなことに、先週公開されたstripe-js v1.4.0のリリースで、よりクリーンな解決策が提供されました。このアップデートでは@stripe/stripe-js/pureインポートパスが導入され、副作用なしでStripeをインポートできるようになりました。

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

これにより、アプリが明示的にloadStripe関数を呼び出すまで、Stripeライブラリの実行が遅延されます。loadStripe関数の呼び出しをStripe決済を伴うページやコンポーネントに限定すれば、Stripeはそれらのページでのみ読み込まれるため、閲覧セッションの早い段階でのユーザー追跡を防ぐことができます。

問題2の解決:決済後に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

前述の2つの手順で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
  • 閲覧中にユーザーがマウスをどれだけ速く動かしたかのタイミング
  • ウェブを横断して同一ユーザーを追跡することを可能にするCookie

Stripeはこのデータ収集について明確に開示しておらず、クライアントアプリケーションがライブラリのトラッキング挙動を制限することを困難にしています。

この問題について非公式に議論する際、Stripeはデータを不正防止と診断のためだけに使用すると公に述べていますが、プライバシーポリシーの文言からは、マーケティング目的で使用または販売している可能性も示唆されます。

Stripeへの提言

Stripeがセキュリティとプライバシーをいかに真剣に考えているかを踏まえると、これほど大量のデータをこれほど透明性なく顧客から収集してきたことは驚きです。これが、気づく顧客があまりに少なかったために続いてきた、単なる見落としであることを願っています。

Stripeがこの状況を是正するために取れる行動はいくつかあります。

  • データ共有を明確に開示すること。
    • npmパッケージのページやStripe.jsのドキュメントで、クライアントアプリケーションが統合した際にライブラリがどのような閲覧データやテレメトリデータをStripeに送信するのかを明確に定義すべきです。
  • ライブラリのアンロードをサポートすること。
    • アプリがユーザーの決済を回収した後にStripeをアンロードするための公式な手段を、クライアントアプリケーションに提供すべきです。
  • オプトインによって共有するデータをクライアントアプリケーションが管理できるようにすること。
    • Stripeのクライアントはアプリケーションに対するチャージバックのコストを負担しているのですから、チャージバックを減らすためにStripeとどれだけの情報を共有するかを自ら決定できるべきです。

更新:2020年4月21日 午後2時47分(米国東部時間)

Stripe共同創業者のPatrick Collison氏がこの記事に返信し、収集したデータを不正検出のためだけに使用するというStripeのコミットメントを改めて表明しました。同氏は、データ収集の慣行に関する利用規約の文言を近く明確化すると付け加えました。

訂正:記事の初版では「Stripeを使って決済を回収するウェブサイトは、StripeのJavaScriptライブラリを組み込まなければならない」と記載していましたが、Collison氏の指摘によれば、これは不正確です。ウェブサイトはStripe JSライブラリを使わずにStripeと統合することも可能だからです。

更新:2020年4月30日

Stripeはプライバシーポリシーと開発者向けドキュメントを改訂し、導入者がトラッキング挙動を制限できる機能をJavaScriptライブラリに追加しました。

Stripeの変更が私が提起した懸念にどう対応したかについて、フォローアップ記事を公開しました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント