Next.js API Route 레이스 컨디션

1회용 쿠폰은 단 한 번만 사용되어야 합니다.

하지만 이 시나리오에서는 30명이 동시에 쿠폰을 사용할 수 있습니다.

이는 Time-of-Check Time-of-Use (TOCTOU) 레이스 컨디션 때문에 발생합니다.

오류는 다음 두 단계에서 발생합니다:

  • 1단계: 서버가 사용 횟수를 확인하기 위해 데이터베이스를 읽습니다.
  • 2단계: 서버가 해당 횟수를 증가시키기 위해 두 번째 명령을 보냅니다.

한꺼번에 많은 요청을 보내면, 서버는 이 두 단계 사이의 중간 과정에서 모든 요청을 처리하게 됩니다.

많은 요청이 카운트가 여전히 0인 상태에서 값을 읽습니다. 모든 요청이 검증을 통과합니다. 모두 할인을 적용합니다. 모두 카운터를 증가시킵니다.

결과는 어떨까요? 한 사람을 위해 만들어진 쿠폰이 30번이나 사용됩니다.

이는 코드 리뷰 중에 놓치기 쉽습니다. 한 줄씩 읽어보면 로직은 완벽해 보이기 때문입니다. 결함은 속도와 동시성을 고려할 때만 드러납니다.

해결 방법:

별도의 읽기 및 쓰기 명령을 사용하는 것을 중단하세요. 대신, 단일 원자적 연산(atomic operation)을 사용하세요.

Prisma에서는 WHERE 절에 조건을 포함한 updateMany를 사용할 수 있습니다:

• 데이터베이스가 조건을 확인하고 단 한 번의 동작으로 업데이트를 수행합니다. • 카운트가 이미 한도에 도달했다면, 업데이트는 즉시 실패합니다. • 확인과 쓰기 사이에 다른 요청이 끼어들 수 없습니다.

또 다른 옵션은 데이터베이스 트랜잭션을 사용하는 것입니다. 이는 데이터를 잠금(lock) 처리하여 작업이 완료될 때까지 다른 요청이 데이터에 접근할 수 없게 합니다.

SQLite의 경우, 단일 업데이트 명령이 가장 신뢰할 수 있는 방법입니다.

항상 스스로에게 질문해 보세요: 만약 두 요청이 동일한 밀리초(millisecond)에 이 로직에 도달한다면 어떤 일이 벌어질까요?

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