価格改ざん・二重課金を防ぐ決済セキュリティ

金額は必ずサーバーで計算し直す。冪等キー、本番/テスト分離、異常額ガードの実践

価格改ざん防止二重課金防止冪等キー環境分離セキュリティ
読了時間: 9分

はじめに

決済まわりのセキュリティというと、通信の暗号化やカード情報の保護を思い浮かべる方が多いと思います。もちろんそれも大切ですが、実際に事故につながりやすいのは、もっと地味な部分です。

「画面に表示された金額をそのまま請求してしまう」「同じ請求が2回走ってしまう」「テスト環境の予約に本番のカードで請求してしまう」。この記事では、そうした落とし穴とその塞ぎ方を紹介します。

金額はブラウザから受け取らない

画面の数字は簡単に書き換えられる

予約画面には合計金額が表示されています。これをそのままサーバーへ送り、その額を請求する作りにすると、ブラウザの開発者ツールで金額を1円に書き換えるだけで、1円で泊まれてしまいます。

金額の扱い方
BEFORE
画面の金額をそのまま請求

ブラウザ側の値は利用者が自由に書き換えられる。1円で予約が成立してしまう

AFTER
サーバーで計算し直して照合

予約管理システムから金額を取り直し、送られてきた値と一致するか確認するだけ

そこで、ブラウザから送られてくる金額は照合用の参考値としてのみ使い、実際の請求額はサーバーが予約管理システムから取り直した値を使います。もし一致しなければ「料金が変更されました」と表示して手続きを中断します。

この設計なら、金額を書き換えられても「照合に失敗して予約が止まる」だけで、被害は発生しません。

オプション料金もサーバーが組み立てる

バーベキュー機材のレンタルといったオプションについても同じです。ブラウザから送られてくるのは「チェックが入っているかどうか」だけで、金額は一切送りません。単価はサーバー側のデータから取得します。

「クライアントから金額を受け取らない」を徹底すると、そもそも改ざんの余地がなくなります。

同じ請求を2回走らせない

冪等キーで守る

通信の途中でタイムアウトすると、実際には決済サービス側で処理が成功しているのに、こちらには失敗として見えることがあります。ここで再送すると、二重課金になります。

決済サービスには、こうした事態のための「冪等キー」という仕組みがあります。リクエストに固有の文字列を添えておくと、同じキーの2回目以降は新しく処理せず、1回目の結果を返してくれます。

今回は予約番号を使って、顧客の作成・決済画面の作成・課金の3か所すべてに冪等キーを設定しました。予約1件につき請求は1回だけ、という保証が仕組みとして得られます。

データベース側でも制約をかける

決済サービス側の対策に加えて、自社データベースにも重複を許さない制約をかけています。予約番号、決済画面の識別子、通知イベントIDのそれぞれに一意制約を設け、同じものを2回登録できないようにしました。

課金の直前にもう一度状態を確認する

自動課金の処理は、まず対象の予約を一覧で取得し、それから1件ずつ請求していきます。この間に、お客様が専用ページからキャンセルするかもしれません。

そこで請求を実行する直前に、もう一度データベースからその予約の最新状態を読み直します。すでにキャンセルされていれば請求しません。一覧を取得した時点の情報を信じ続けない、という考え方です。

本番とテストを取り違えない

同じデータベースを共有する怖さ

開発中は、テスト用の決済環境で動作確認をします。このとき本番用のデータベースを共有していると、テストで作った予約に対して本番の自動課金処理が動いてしまう危険があります。

実際、この問題は開発中に発生しました。本番の定期処理が、テスト環境で作られた予約を課金対象として拾ってしまったのです。

予約データに「どちらの環境か」を記録する

対策として、予約データそのものに「本番の決済で作られたか、テストで作られたか」を記録する項目を追加しました。課金処理は、今動いている環境と一致する予約しか処理しません

環境の食い違いを防ぐ多層防御
鍵の形式を検証

本番環境なら本番用の鍵、それ以外ならテスト用の鍵しか受け付けない

予約に環境を記録

予約を作成した時点で、どちらの環境かをデータに残す

課金時に照合

環境が一致しない予約は、エラーにせず静かに対象外として飛ばす

ポイントは、環境が一致しない予約を「エラー」ではなく「対象外」として扱うことです。エラーにすると運営者にアラートメールが飛び、テストのたびに通知が届いてしまいます。

鍵が正しくないときは機能自体を隠す

決済の鍵が未設定だったり、環境と食い違っていたりする場合、予約ボタン自体を表示しないようにしました。お客様が予約に進んでから「エラーが発生しました」と表示されるより、最初から予約できないほうがずっと親切です。

情報を持たない・出さない

決済サービスに渡す情報を絞る

決済サービスに渡す付帯情報(メタデータ)には、予約番号と日程だけを含め、氏名・電話番号・住所は一切渡しません。決済に必要のない個人情報を外部サービスのログに残さないためです。

同様に、エラーメッセージやログにも、カード情報・トークン・ゲスト専用ページのURLは出力しません。ログに残った情報は、それ自体が漏洩経路になり得ます。

定期処理の入り口も守る

自動課金などの定期処理は、URLを知られると誰でも実行できてしまいます。そこで合言葉による認証をかけ、しかも合言葉が未設定の場合は実行を拒否する設定にしました。

「設定を忘れたら認証なしで通ってしまう」という作りは、事故のもとです。設定がなければ動かない、という方向に倒しておくのが安全です。

まとめ

決済セキュリティで押さえたのは、次の3点です。

  1. 金額をクライアントから受け取らない — 送られてきた値は照合専用、請求額はサーバーが取り直す
  2. 重複を二重に防ぐ — 決済サービスの冪等キーと、データベースの一意制約を併用する
  3. 環境の取り違えを構造で防ぐ — 予約データに環境を記録し、一致しないものは処理しない