Post

프로젝트 부적절한 요청 대응

회원으로 운영되지 않는 서비스였기 때문에 사용자가 API스펙만 알아낸다면 손쉽게 부적절한 요청을 적용시킬 수 있습니다. 이러한 환경에서 어떻게 부적절한 요청을 사전에 방지하고, 발생했다면 어떻게 올바른 데이터로 복구할 수 있었는지 설명합니다.

개요

회원이 없는 서비스에서, 일부 사용자가 브라우저 개발자 도구를 이용해 API 스펙을 분석하고 의도적으로 악의적인 요청을 보낸 사례가 발생했습니다. 대표적인 사례 중 하나가 도전기록 조작입니다. 각각의 아이템은 가장 높은 가치로 강화된 기록 정보를 가지고 있는데, 이 도전 기록을 조작하여 터무니 없이 높은 점수로 기록을 올려버린 것입니다. 이를 방지하기 위해서 다음과 같은 조치를 취했습니다.

  1. 서버 측에서 요청하는 데이터의 정합성을 검사하는 로직을 추가.
  2. 도전 기록 갱신정보를 저장하는 로그 테이블 추가.
  3. 로그를 기반으로 기존 도전기록을 복구할 수 있는 시스템 구축.
  4. 부적절한 요청을 시도한 사용자 IP 차단.

기능설명

데이터 정합성 검사 로직

사용자 요청에 담긴 데이터의 정합성을 검사하는 로직을 담았습니다. 검사에 통과하지 못하면 실패응답을 전달합니다. 여기서는 기록 등록 기능에서 어떤 기준으로 데이터를 검사했는지 예를 들어보겠습니다.

기록 등록 기능은 가장 높은 가치로 강화된 기록을 등록하면서 다른사람과 경쟁할 수 있는 기능입니다. 위에서 설명드린 바와 같이 터무니 없이 높은 점수로 서버에 요청해 부적절한 기록을 세우는 경우가 있었습니다. 이를 방지하기 위해서 기록을 등록하기 전에 다음과 같은 검사를 수행하였습니다.

  1. 강화 성공횟수 검사: 각 아이템마다 강화할 수 있는 횟수가 제한되어 있습니다. 사용자의 요청에 담겨있는 성공횟수가 이 수치를 넘어가는지 검사합니다.
  2. 주문서 적합성 검사: 각 아이템마다 적용할 수 있는 주문서(강화할 때 사용되는 아이템)의 종류가 정해져있습니다. 사용자의 요청에는 사용자가 사용한 주문서의 정보도 함께 전달되는데, 해당 주문서가 특정 아이템에 적용될 수 있는 것인지 검사합니다.
  3. 강화 수치 검사: 요청에는 어떤 주문서를 얼마나 성공시켰는지에 대한 정보가 포함됩니다. 그리고 각 주문서가 올려주는 능력치의 종류와 수치가 각각 다릅니다. 요청에 담겨있는 능력치 강화 수치와 서버에서 계산된 수치가 일치하는지 비교합니다.

도전기록 로그 테이블

특정 기록에 도전해서 갱신에 성공하면 관련된 데이터가 로그 테이블에 저장됩니다.

  • 어떠한 아이템의 기록갱신에 성공했는가.
  • 언제 등록된 기록인가.
  • 누가 기록을 등록했는가.
  • 등록된 점수는 몇점인가.

등등 여러가지 정보가 로그 테이블에 기록됩니다.

복구 시스템

누군가 부적절한 요청으로 이상한 기록을 다수 만들어 둔 것을 관리자가 삭제하고, 기존의 기록을 복구하는 시스템을 구축하였습니다. 기록을 남긴 사용자의 IP정보도 로그에 기록되기 때문에 특정 사용자가 만들어둔 기록을 무시할 수 있습니다. 이상한 기록을 남긴 사용자의 로그와 기록정보를 모두 삭제합니다. 그리고 로그는 존재하는데 기록데이터가 존재하지 않거나, 로그와 기록데이터에 괴리가 있는 경우를 체크해서 복구를 수행합니다.

사용자 차단

로그에 남은 사용자의 IP정보를 이용해서 해당 사용자의 모든 요청을 차단하도록 했습니다. 관리자 페이지에서 특정 사용자의 IP 차단요청 서버에 전달할 수 있습니다. 요청을 받은 서버는 차단 IP 목록을 저장하는 테이블에 전달받은 IP를 추가합니다.

서버가 주기적으로 데이터베이스로부터 차단 IP 목록을 불러와 메모리에 저장합니다. 차단된 IP로 요청이 온다면 스프링 인터셉터에서 사용자의 요청을 처리하지 않고 403 Forbidden 응답을 반환합니다.

IP주소를 메모리에?

문자열로 저장된 IP주소 하나의 최대 크기는 15 x 8byte = 120byte (숫자 12글자 + . 3글자) 입니다. 10,000개의 IP주소가 메모리에 저장된다고 해도 메모리에서 차지하는 공간은 1,200,000 byte ≈ 1.2MB 에 불과합니다. 서비스의 규모가 아직 작아 차단해야하는 IP목록이 많지 않고, 10,000개 정도의 IP 목록을 메모리에 저장한다고 해도 서버의 자원에 미치는 영향은 미비하다고 보아 메모리에 저장하는 방식을 선택했습니다.

물론 위의 경우는 차단될 IP주소의 수가 10,000개 이하일 것을 가정한 것입니다. 100만개, 1000만개 정도로 갯수가 늘어나는 경우에는 Redis와 같은 외부 저장소를 사용하는 방법을 생각하고 있습니다. Redis가 사용하는 메모리 용량으로도 부족하다면 외부 스토리지와 결합하거나 IP주소를 문자열이 아닌 비트맵과 같은 다른 형식으로 저장하는 방법을 사용해야 할 것 같습니다.

This post is licensed under CC BY 4.0 by the author.