Stripe is Silently Recording Your Movements On its Customers' Websites

Michael Lynch

Stripeが顧客のWebサイト上であなたの行動を密かに記録している

スタートアップやテック企業のあいだでは、決済処理といえばStripeがほぼ標準といっても過言ではありません。私も新しいWebアプリに有料サブスクリプション機能が必要になったとき、自然な選択肢としてStripeを選びました。ところが統合してみると、Stripeの公式JavaScriptライブラリが、私のサイト上でのあらゆる閲覧行動を記録し、Stripe側に送信していることが分かりました。そこに含まれるデータは、たとえば次のようなものです。

  1. ユーザーがサイト内で訪れたすべてのURL。Stripeの決済フォームが一度も表示されないページも含みます
  2. サイト閲覧中のマウスカーソルの動きに関するテレメトリー
  3. Stripeを導入している他のサイトと照合して、サイト訪問者を紐づけられる一意の識別子

この記事では、私が見つけたこと、誰に影響があるのか、そしてWebアプリケーションでStripeのデータ収集を抑える方法について共有します。

影響を受けるのは誰か

次のいずれかに当てはまる場合、StripeはあなたのWebサイト上でこのデータを収集しています。

  • ベースとなるページテンプレートにStripeのscriptタグが含まれている場合:
    • <script src="https://js.stripe.com/v3">
  • Webサイトが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フラグメント(例:/account?id=12345#name=michael)まで含めた完全なURLを記録しています。こうした部分に機微な情報を入れて使うWebサイトも存在します。

動画でお気づきかもしれませんが、アプリを最初に読み込んだときは最初のページでリクエストが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を導入している他のWebサイトを訪れた際にもユーザーを追跡できるようになります。

これはミスなのか

当初、これはきっと自分のミスだと思いました。Stripe統合のどこかで不注意な実装をしてしまい、誤って外部送信しているのだろうと考えたのです。

調べるため、アプリからHTTP POSTリクエストが送られていた次のURLを検索してみました。

https://m.stripe.com/4

すると、自分のコードのバグではなく、同じ挙動に驚いた他の開発者による投稿が何十件も見つかりました。報告は2017年までさかのぼります

Stripeのトラッキング挙動についての過去のユーザー報告を集めたコラージュ

開発者たちは何年も前からStackOverflowやGitHubでStripeのトラッキング挙動について投稿しています

GitHubのissueスレッドのひとつでは、Stripeの従業員がこの挙動は意図したものではないとし、修正を検討するとコメントしていました。

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

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

それから7か月が経ちましたが、そのスレッドにも、他に見つけられる場所にも、Stripeからの続報はありませんでした。

問題の再現

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

実験の結果、次の1行だけでStripeライブラリが読み込まれ、ユーザーのトラッキングが開始されることが確認できました。

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

注目すべきは、アプリがloadStripe関数を一度も呼び出していないという点です。Stripe.jsは、クライアントアプリがライブラリをimportしただけでユーザーの行動の記録を開始します。シングルページアプリケーションの場合、エンドユーザーがWebサイトのいずれかのページを読み込んだ瞬間にこれが発生します。

注記: loadStripeという名前は誤解を招きます。クライアントアプリケーションがこの関数を呼び出す前からStripeはすでに読み込まれているからです。より適切な名前はensureStripeIsLoadedでしょう。この関数の実際の役割は、Stripeライブラリの読み込みが完了するまでアプリ側のAPI呼び出しをキューに入れておくことだからです。

Stripeへの報告

この問題が意図した挙動なのか、どうすれば防げるのかを確認するため、Stripeサポートに報告しました

Stripeからはすぐに返信があり、ユーザーのトラッキングは仕様であり、むしろ歓迎すべき機能だと説明されました。

Michaelさん、こんにちは。

お問い合わせありがとうございます。StripeサポートのFaithです。

早速ですが、ご確認いただいた呼び出しは不正利用を検出するために意図された仕様であり、ユーザーの利益になるものです。ドキュメントにも次のように記載されています。「Stripeの高度な不正利用対策機能を最大限に活用するには、このスクリプトをチェックアウトページだけでなく、すべてのページに含めてください。これにより、顧客がWebサイトを閲覧する際の不正を示す可能性のある異常な行動をStripeが検出できるようになります。」

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

他にご不明な点やご懸念がありましたら、お知らせください。

よろしくお願いいたします。
Faith

「ユーザーの利益になる」という一文は、特に恩着せがましく感じられました。このデータ収集で最も利益を得ているのは明らかにStripeであり、ユーザーではありません。Stripeは不正検出モデルの学習用データを無償で手に入れ、その情報を広告主に販売している可能性すらあります。

ユーザーにとっては、Stripe.jsは余計なJavaScriptライブラリのダウンロードを強い、ブラウザから余計なHTTPリクエストを送信させます。しかも、ユーザーがクレジットカード決済を受け付けるページを一度も訪れなくても発生するのです。

Stripeはこのことを開示しているのか

Stripeによるこの挙動について公式な開示を探してみましたが、見つかりませんでした。最も近いものは、Stripeサポートの担当者が引用していた、npmパッケージの説明にある次の曖昧な一文でした。

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

プライバシーポリシーには、収集するデータについてもう少し具体的な記述がありますが、顧客のサイトではなくstripe.com上で収集しているかのような書きぶりになっています。

当社のサイトでは、効果的に機能させるためにCookieやその他の技術を使用しています。これらの技術は、当社サイトの利用に関する次のような情報を記録します。

  • ブラウザやデバイスのデータ(IPアドレス、デバイスの種類、OSやインターネットブラウザの種類、画面解像度、OS名とバージョン、デバイスのメーカーやモデル、言語、プラグインやアドオン、アクセスしているサイトの言語バージョンなど)
  • 利用状況データ(サイトでの滞在時間、閲覧したページ、クリックしたリンク、言語設定、当社サイトに至るまでに経由したページなど)

また、当社は時間をかけて、Webサイトや接続されたデバイス上でのオンライン行動に関する情報を、サードパーティのWebサイト、デバイス、アプリ、その他のオンライン機能やサービスを横断して収集する場合もあります。

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

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

対策

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

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

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

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

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

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を使って決済を回収するWebサイトでは、通常アプリケーション内に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日 14時47分(米東部時間)

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

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

更新:2020年4月30日

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

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

原文は Michael Lynch により に公開されました。

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