캐쥬얼 게임의 백엔드 API 보안
캐쥬얼 게임 등에서 사용하는 API에 대한 보안처리
과거에 비실시간형 Http 기반의 게임 서버 개발을 해오다가 현재는 플랫폼 쪽 개발만 담당하고 있었다. 최근 우연한 계기로 Java 기반으로 개발된 게임 서버쪽 소스를 구경하게 되었는데 오래전에 직접 게임 서버 담당으로 개발 했던 기억이 새록새록 나게 되어 추억팔이를 하며 그때의 고민거리를 기록으로 남기려고 한다.
캐쥬얼 게임 서버 API의 경우 대부분이 클라이언트의 결과 기록성 쓰기 기능들이 많이 있는데, 이 때문에 API의 멱등성이나 Replay를 막는 처리들이 일반적인 웹 API에 비해서 까다롭게 처리해야 한다. 예를 들어 일반적인 웹 서비스에서 글쓰기 기능은 여러번 Replay 되더라고 여러개 글이 작성되고 끝이지만, 게임의 대전 기록을 남기거나 어떤 구매를 진행함에 있어 여러번 처리되는 일은 서비스에 치명적이다. (물론 웹 서비스들도 그런 것들이 있다.) 여기에서는 주로 중복 요청 (Replay attack)에 대한 보안처리를 생각했던 것을 다룬다.
Replay attack이란?
Replay 공격은 유효한 데이터 전송이 악의적으로 또는 부정하게 반복되거나 지연되는 네트워크 공격의 한 형태입니다. 이것은 IP 패킷 대체에 의한 스푸핑 공격의 일부로 데이터를 가로채서 다시 전송하는 공격자 또는 발신자에 의해 수행됩니다.
요청을 수행하는 주체가 유저 본인도 모르는 사이의 본인일 수도 있고 요청 정보를 가로챈 공격자 일 수도 있지만, 두가지를 다 고려하여 무조건적으로 한종류의 요청은 한번만 처리되게 구현하는 것을 목표로 한다.
이러한 Replay 공격을 방어하는 방법은 과거에서부터 몇 가지가 제시되고 있는데 다음과 같다.
1. 순서 번호(sequence number)
송신 메시지에 매회 1씩 증가하는 번호를 함께 전달
현재 시퀀스와 송신 시퀀스를 비교하여 불일치할 경우 반려처리
2. 타임스탬프(timestamp) 사용
송신 메시지에 현재 시각을 함께 전달.
현재 시각과 송신 시간을 비교하여 일정시간이 지난 송신에 대한 반려 처리
3. 비표(nonce) 사용
메시지를 수신하기에 앞서 수신자는 송신자에게 일회용의 랜덤한 값(nonce) 전달
매번 특정 입장티켓을 발급해주고 이를 통해 입장시키는 형태
과거에 내가 개발할 때는 1번 방법을 사용하는 방식으로 처리했었다.
요즘의 일반적인 웹 API의 경우 Spring security와 JWT 등의 조합으로 인증과 권한 처리까지 대부분 하는데, 캐쥬얼 게임에서 사용하는 API는 권한이 필요한 경우가 없기도 하고 이정도 까지는 안해도 된다 생각했었다. 그래서 간단하게 API Token 방식과 이를 받아 검증하고 통과시키는 수준의 Filter정도만 구현해서 사용했었던 것 같다.
유저 인증 정보인 API Token은 Redis HSET으로 처리하여 유저 세션처럼 접근하여 사용하였다. 그당시에는 1번 방법이 같이 적용되어 저장되는 데이터는 얼추 이런 형태가 됐었다.
Key | Field | Value |
---|---|---|
token | uid | msseol |
token | seq | 0(순차적증가) |
보면 바로 알 수 있듯이 인증 token을 키로 갖고, 특정 유저의 식별자인 uid와 시퀀스 정보를 저장하는 형태로 되어있다. 여기에서 seq는 곧, 요청하는 API의 순번을 뜻한다고 생각하면 된다.
규모가 있는 서비스의 경우 단일 Redis로 처리가 벅찰 수 있는데, 별도 클러스터 환경으로 구축하거나 ElastiCache를 사용하는게 안정적이다.
클라이언트와 서버의 API 처리 과정
1. 클라이언트는 매 요청 시 token과 seq를 서버로 전달한다.
HTTPS /api/blahblah
-H 'X-API-TOKEN=xxx'
-H 'X-API-SEQ=0'
...
2. token은 Redis 세션으로 사용한다. (필터/인터셉터)
3. 전달받은 seq는 Redis에 저장되어 있는 seq와 비교한다.
요청seq | 레디스seq상태 | 처리결과 |
---|---|---|
0 | 0 | 성공 |
0 재시도 | 1 | 실패 |
1 | 1 | 성공 |
4. 같다면 유효한 요청으로 처리하고 Redis의 seq를 증가시킨다.(HINCRBY)
5. 서버는 증가시킨 seq를 클라이언트에 응답해주고 클라이언트는 이를 저장한다.
시퀀스 방식으로 진행하면 요청 탈취로 인한 중복 요청도 막을 수 있고, 클라이언트 더블클릭 등의 중복 요청 처리도 막아지는 장점이 있다.
다만, 시퀀스라는 의미에 맞게 순차적으로 처리되는 요청에만 사용할 수 있는 단점이 있다. 무언가 비동기적으로 여러 요청이 필요한 게임 서비스라면 이방법보단 요청 Timestamp를 비교하여 요청의 만료여부를 판단할 수 있게 하는 방법이 낫다. (물론 이경우에는 짧은시간에 고의적인 반복요청을 막을 순 없다.)
그리고 현재의 구조에서는 공격자가 요청을 수집하여 현재에 맞는 seq를 찾아서 loop등을 통해 찾으면 정상적인 요청으로 간주될 우려가 있다. seq 변조에 대한 처리가 별도로 없기 때문이다. 이 경우는 인증정보와 시퀀스를 포함하여 생성한 HMAC 정보를 추가로 전송하면 탈취후 변조되더라도 막을 수는 있다. 암호화 키가 노출되지만 않는다면 말이다.
반드시 적용되어야 할까?
대부분의 웹 서버가 그렇듯 API의 설계 자체를 멱등성있게 설계할 수 있다면 적용할 필요는 없다. 방을 생성할 때 DB 수준에서 한 유저는 하나의 방만 존재할 수 있도록 유니크 하게 처리한다던지, 업데이트를 할 때 멱등성있게 업데이트 되는 그런 방식이다. 요청 탈취는 ssl로도 대비가 되고 token 유효기간 단축으로 효과를 볼 수 있다. 이런식의 처리로도 특별한 경우를 제외하고는 어느정도 문제없는 처리가 가능하다고 생각한다. 또한 작은 회사에서는 Redis와 같은 추가 리소스 소모 또한 부담이기에, 중요도와 구현 방식에 따라 여러가지로 고려해보고 적용해도 될 것 같다.