مرکزی مواد پر جائیں
友田 陽大
Running the app you built with AI
バイブコーディング
決済
セキュリティ
個人開発
信頼性

AIに決済を作らせたとき、二重課金が起きる理由と防ぎ方

AIが書いた決済処理で顧客が二重に課金される仕組みを、非エンジニア向けに解説します。Webhookが複数回届くのは仕様であること、冪等性という対策、金額をブラウザから送ってはいけない理由、署名検証の必要性。本番二重課金0件を維持した決済基盤の実装知見に基づきます。

Published
Reading time
10 min read
Author
友田 陽大
شیئر کریں

お金が絡むと、間違いのコストが変わります。データが見えてしまうのも深刻ですが、二重に課金された顧客は、その場で怒って、二度と戻ってきません。返金はできても、信用は戻りません。

そして厄介なことに、この不具合は手元のテストでは絶対に再現しません。本番で、しかも一番忙しい時間帯に初めて出ます。

決済機能が無いアプリの方は、この記事は飛ばして構いません。


1. まず、何が起きるのか

Stripe などの決済サービスを使うと、こういう流れになります。

  1. 顧客が「購入」を押す
  2. 決済サービスの画面でカード情報を入力し、支払いが完了する
  3. 決済サービスがあなたのアプリに「支払われましたよ」と通知を送る(Webhook と呼びます)
  4. あなたのアプリがその通知を受けて、商品を渡す・残高を増やす・注文を確定する

問題は 3 です。

この通知は、同じ内容で複数回届くことがあります。

そして 4 の処理が「通知を受けるたびに残高を足す」形だと——2回届けば2回足されます。それが二重付与です。


2. なぜ複数回届くのか — これは仕様です

「通知が2回来るなんて、バグでは?」と思うかもしれません。違います。設計です。

決済サービスにとって最悪の事態は、通知が届かないことです。支払いは完了したのに商品が渡されない——これは決済サービスの信頼を根本から損ないます。

だから彼らはこう設計しています。

確実に1回届けるより、最低1回は届ける

これを at-least-once(最低1回)配信 と呼びます。次のような場合に再送が起きます。

  • あなたのアプリの応答が遅かった(タイムアウト扱いになる)
  • あなたのアプリが一時的にエラーを返した
  • ネットワークが不安定だった
  • 決済サービス側の内部的な再試行

Stripe の公式ドキュメントにも、イベントが複数回配信され得ることと、受け取る側が冪等に処理すべきことが明記されています。

つまり——二重に届けないのは決済サービスの仕事ではなく、二重に処理しないのがあなたの仕事です。契約としてそうなっています。


3. なぜAIは書き漏らすのか

「支払いが完了したら残高を増やして」と頼めば、AI はその通りに書きます。それは正しい実装です。

ただし「同じ通知が2回来たらどうするか」は、頼まれていないので考慮されません。

これは意地悪でも手抜きでもなく、指示の範囲外だからです。そして——

手元での動作確認では、通知は1回しか来ません。

自分でテスト購入すると、通知は1回。正常に動く。何度やっても正常に動く。二重処理の分岐は一度も実行されないからです。

Veracode の検証でAI生成コードの45%に脆弱性が見つかったという結果がありますが、この種の「エラーは出ないが本番でだけ壊れる」欠陥は、静的な検査でも捕まえにくい部類です。設計として最初から入れておくしかありません。


4. 対策 — 冪等性という考え方

対策には名前があります。冪等性(べきとうせい、idempotency)

何回やっても結果が同じになる性質

エレベーターのボタンを5回押しても、エレベーターは1回分しか来ません。これが冪等です。押した回数だけエレベーターが来たら困ります。

決済処理でこれを実現する方法は、実はとても単純です。

やること

  1. 決済サービスからの通知には、イベントIDevt_ で始まる一意の文字列など)が含まれている
  2. 処理する前に、そのIDがすでに処理済みかどうかをデータベースで確認する
  3. 処理済みなら、何もせずに「受け取りました」と応答する
  4. 未処理なら、処理してからIDを記録する

これだけです。難しくありません。忘れられるだけです。

実装上の要点3つ

(1)記録と業務処理は同じトランザクションにまとめる

「残高を増やす」と「IDを記録する」を別々に行うと、その間でアプリが落ちたときに、残高は増えたのにIDが記録されていない状態になります。次の通知でまた増えます。この2つは必ず一括で成功か失敗かにします。

(2)IDの列に「一意制約」を付ける

「確認してから書き込む」の間に、もう1つの通知が同時に入ることがあります(競合状態)。データベース側で「同じIDは2行入れられない」という制約を付けておけば、後から来たほうが確実に弾かれます。アプリ側のチェックだけに頼らない、というのが要点です。

(3)返すステータスコードを間違えない

処理済みで何もしなかった場合も、成功(200)を返します。エラーを返すと、決済サービスは「まだ届いていない」と判断してさらに再送します。

逆に、本当に処理に失敗したときはエラーを返します。ここで成功を返してしまうと、決済サービスは「届いた」と判断して再送をやめ、通知が永久に失われます。

実案件での確認

私が担当した決済基盤では、この設計により本番での二重課金を0件に保っています。4つの面(顧客・加盟店・管理・店頭端末)を横断する規模の決済プラットフォームでの実績です。

強調しておきたいのは、これが技術的に高度な話ではないことです。「必ずやる」と決めているかどうかの差でしかありません。


5. もう2つの落とし穴

決済まわりには、二重課金以外にもよくある穴が2つあります。

落とし穴1: 金額をブラウザから受け取っている

こういう実装を、AI は書きがちです。

【危険】
ブラウザ: 「商品Aを、5000円で購入します」
サーバー: 「はい、5000円で決済を作成します」

画面の状態をそのままサーバーに渡す——素直で、正常に動きます。

しかしブラウザから送られてくる値は、利用者が自由に書き換えられます。開発者ツールを使えば、50001 に変えて送ることができます。

【正しい】
ブラウザ: 「商品Aを、1個購入します」
サーバー: 「商品Aの価格をデータベースで確認 → 5000円 → 決済を作成」

受け取るのは商品IDと数量だけ。価格はサーバー側で引き直す。 これが原則です。クライアントから来た金額は、絶対に信用しません。

あわせて、数量の上限(マイナスや異常な大きさを弾く)と在庫の確認も、サーバー側で行う必要があります。「数量 -1」で購入されると、返金が発生することがあります。

落とし穴2: 署名を検証していない

Webhook の受け口は、インターネットに公開されています。URLさえ分かれば、誰でもリクエストを送れます。

署名を検証していないと、こうなります。

  1. 攻撃者があなたのWebhookのURLを見つける(推測しやすい形が多い)
  2. 「支払いが成功しました」という形のデータを、自分で組み立てて送る
  3. あなたのアプリは、それを本物だと信じて商品を渡す

決済サービスは、通知に署名を付けています。これを検証すれば、本物かどうか判定できます。

実装上の注意: 署名の検証には、フレームワークが解析する前の「生のリクエストボディ」 が必要です。JSONとして解析してしまうと、文字列が微妙に変わって検証に失敗します。AI がこの点を見落とすと、「署名検証を入れたのに全部失敗する」という状態になり、そこで検証を外してしまう——という最悪の流れが起こり得ます。


6. 自分で確認する手順

コードが読めなくても、いくつかは確認できます。

確認1: 金額の改ざん

  1. 購入画面を開き、F12 で開発者ツールの「ネットワーク」タブを開く
  2. 購入ボタンを押す
  3. サーバーへ送られたリクエストの中身を見る

送信内容に「金額」「価格」「amount」「price」といった項目が含まれていたら要注意です。商品IDと数量だけなら正常です。

確認2: Webhookの署名検証

これはコードを見ないと分かりませんが、AI に聞けば答えられます(次章の指示文を使ってください)。

確認3: 二重処理

Stripe の管理画面には、Webhook イベントを手動で再送する機能があります。

  1. Stripe ダッシュボード → Developers → Webhooks
  2. 過去のイベントを選ぶ
  3. 「Resend(再送)」を実行する
  4. あなたのアプリのデータを確認する

残高が二重に増えた、注文が2つできた——なら、二重処理されています。何も変わらなければ、冪等性が効いています。

これが唯一の実地確認方法です。 公開前に必ず1回やってください。


7. AIに貼る修正指示

このアプリの決済まわりを、次の4点について確認して報告してください。

(1) Webhook の処理で、同じイベントが複数回届いたときに二重処理される
    可能性があるか
(2) 決済金額がクライアント(ブラウザ)から送られた値を使っていないか
(3) Webhook の署名検証が行われているか
(4) 数量や在庫の検証がサーバー側で行われているか

そのうえで、次のように修正してください。

■ 冪等性
イベントIDを一意キー(データベースの unique 制約付き)として保存し、
処理済みなら早期リターンする実装を追加してください。イベントIDの記録と
業務処理(残高の更新・注文の確定など)は同一トランザクションにまとめて
ください。処理済みで何もしなかった場合は 200 を返し、本当に処理に失敗
した場合のみエラーを返して決済サービスが再送できるようにしてください。

■ 金額
クライアントから金額を受け取っている場合は、商品IDと数量だけを受け取り、
価格はサーバー側でデータベースから引き直す実装に変更してください。数量
の上限とマイナス値の検証も追加してください。

■ 署名検証
未実装なら、決済サービス公式の署名検証を実装してください。その際、
フレームワークが本文を解析する前の生のリクエストボディを使うように
し、検証に失敗したら処理せず 400 を返してください。

私はコードが読めないので、各項目について「修正前は何ができてしまう
状態だったか」を日本語で1行ずつ説明してください。

8. 修正したあと、必ず再送テストを

AI が「修正しました」と言っても、それだけでは確認になりません。

Stripe の管理画面から、実際にイベントを再送してください。 データが二重にならなければ、冪等性が効いています。

このテストは1分で終わり、しかも本番で最も痛い事故を1つ完全に潰せます。費用対効果で言えば、この記事のどの作業よりも高いはずです。


まとめ

  • 決済サービスのWebhookは「最低1回」届く設計。複数回届くのは仕様であって障害ではない
  • 二重に届けないのは決済サービスの仕事ではなく、二重に処理しないのがあなたの仕事
  • 対策は冪等性。イベントIDを一意キーで保存し、処理済みなら何もしない。記録と業務処理は同一トランザクションで
  • 金額はブラウザから受け取らない。商品IDと数量だけ受け取り、価格はサーバー側で引き直す
  • Webhookの署名検証は必須。生のリクエストボディを使う点に注意
  • 確認方法は Stripe 管理画面からのイベント再送。公開前に必ず1回

決済は、AI で作ったアプリで最も実害が出やすい領域です。同時に、対策は定型的で、一度入れれば終わりです。

他に何を確認すべきかは AIで作ったアプリを公開する前に にまとめています。

عمومی سوالات

なぜ決済の通知が複数回届くのですか?
決済サービスは「通知が確実に届く」ことを優先しているためです。ネットワークが不安定だったり、あなたのアプリの応答が遅れたり、一時的にエラーを返したりすると、決済サービスは『届かなかったかもしれない』と判断して再送します。これは at-least-once(最低1回)配信と呼ばれる設計で、Stripeの公式ドキュメントにも明記された仕様です。障害ではありません。
冪等性(べきとうせい)とは何ですか?
「何回やっても結果が同じになる性質」のことです。エレベーターのボタンを5回押しても1回分しか動かないのと同じ考え方です。決済処理では、通知に含まれるイベントIDをデータベースに保存し、すでに処理済みのIDが来たら何もせず正常応答を返すことで実現します。
手元でテストすれば二重課金は見つかりますか?
見つかりません。手元での動作確認では通知は1回しか来ないため、二重処理の分岐は一度も実行されません。この不具合は本番環境で、しかもネットワークが不安定なときや処理が混み合ったときに初めて出ます。テストで見つからないことが、この問題を厄介にしている最大の理由です。
金額をブラウザから送ると何が問題ですか?
リクエストを書き換えるだけで、好きな金額で購入できてしまいます。ブラウザから送られてくる値は、利用者が自由に変更できるからです。AIは画面の状態をそのままサーバーに渡す実装を書きがちで、通常の操作では正常に動くため気づきにくい欠陥です。対策は、受け取るのを商品IDと数量だけにして、価格はサーバー側でデータベースから引き直すことです。
Webhookの署名検証はなぜ必要ですか?
Webhookの受け口はインターネットに公開されているため、URLさえ分かれば誰でもリクエストを送れるからです。署名を検証していないと、第三者が「支払いが成功しました」という偽の通知を送るだけで、商品や残高をタダで受け取れます。決済サービスが提供する署名検証の関数を使い、検証に失敗したリクエストは処理せずエラーを返してください。

حوالہ جات

友田

友田 陽大

وزیر (METI) ایوارڈ یافتہ پروڈکٹ کے ڈویلپر۔ TypeScript + Python + AWS کے ساتھ میں SaaS، صنعتی ڈیجیٹل تبدیلی اور پروڈکشن کے قابل جنریٹو اے آئی (RAG) تنہا ابتدا سے انتہا تک فراہم کرتا ہوں — تقاضوں کی تعیین سے انفراسٹرکچر اور آپریشن تک۔

Already live, or handling money or personal data?

An engineer reads your AI-built app and tells you what is actually dangerous

No coding knowledge required. I read the repository, list what to fix first in priority order, and hand back a report written for non-engineers plus fix instructions you can paste into your AI tool. I build production B2B SaaS with Claude Code myself, so I am not going to tell you that using AI was the mistake.

پروجیکٹ کی بنیاد پر کام اور تکنیکی مشاورت دونوں کے لیے دستیاب ہوں۔ 30 منٹ کی مفت مشاورت سے آغاز کریں۔

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

یہ بھی پڑھنے کے قابل