Next.js API ルートにおけるレースコンディション

1回限りのクーポンは、一度しか使用できないはずです。

しかし、このシナリオでは、30人が全く同時にそれを使用できてしまいます。

これは、Time-of-Check Time-of-Use (TOCTOU) レースコンディションによって発生します。

このエラーは、次の2つのステップで発生します:

  • ステップ 1: サーバーがデータベースを読み取り、使用回数を確認する。
  • ステップ 2: サーバーがその回数をインクリメントするための2つ目のコマンドを送信する。

同時に大量のリクエストを送信すると、サーバーはこれら2つのステップの途中でそれらすべてを処理してしまいます。

多くのリクエストが、カウントがまだゼロである間に読み取ります。それらはすべてチェックを通過し、割引を適用し、カウンターをインクリメントします。

その結果? 1人用のクーポンが30回も使用されてしまうのです。

これはコードレビューで見落としやすいポイントです。一行ずつ読んでいくと、ロジックは完璧に見えます。欠陥は、速度と並行性を考慮したときにのみ明らかになります。

修正方法:

読み取りと書き込みのコマンドを別々に使うのをやめましょう。代わりに、単一のアトミックな操作を使用します。

Prismaでは、WHERE句に条件を指定した updateMany を使用できます:

• データベースは条件を確認し、単一の動作で更新を実行します。 • カウントがすでに上限に達している場合、更新は即座に失敗します。 • チェックと書き込みの間に、他のリクエストが入り込むことはできません。

もう一つの選択肢は、データベーストランザクションを使用することです。これによりデータがロックされ、処理が完了するまで他のリクエストがデータに触れることができなくなります。

SQLiteの場合、単一の update コマンドが最も信頼性の高い方法です。

常に自問してください。「もし2つのリクエストが同じミリ秒にこのロジックに到達したら、何が起こるだろうか?」と。

出典: https://dev.to/oopssec-store/racing-a-nextjs-api-route-coupon-abuse-with-prisma-and-sqlite-3gma