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