43. 인증 완료 전 도착 패킷의 미인증 처리와 상태 게이트 부재
난이도 중내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 인증 완료 전 도착 패킷의 미인증 처리와 상태 게이트 부재
난이도: 중상
요약
OnReceive 의 default 분기 (B) 는 세션 상태를 확인하지 않고 게임 명령을 즉시 처리한다.
클라가 접속 직후 인증 응답을 기다리지 않고 MOVE/USEITEM 을 보내면, 인증이 끝나기 전(AccountId==0)
그 명령이 실행된다. 게다가 (A) 의 인증 완료 처리는 비동기 콜백에서 신원과 상태를 순서 없이 채워,
"상태만 먼저 보이고 신원은 아직" 인 창을 만든다.
문제점
- [상태 게이트 부재] 미인증 명령 실행:
(B)는session.State를 검사하지 않는다. 접속 직후 보낸 게임 패킷이State == Connecting/Authenticating인데도_handler.Handle로 들어간다. 핸들러는session.AccountId(기본 0)를 사용하므로, 존재하지 않는/잘못된 계정에 이동·아이템 사용이 적용되거나, 0번 계정 데이터를 건드려 오염·예외가 난다. 인증 우회(권한 상승) 통로가 된다. - [발행 순서·가시성] 신원과 상태의 비원자 게시:
(A)는 별도 스레드(ContinueWith 콜백)에서AccountId를 먼저 쓰고State를 나중에 쓴다. 수신 스레드가State == Authenticated를 본 시점에AccountId쓰기가 반드시 보인다는 보장이 없다(메모리 가시성/재배열). 상태 게이트를 추가하더라도 이 발행 순서를 동기화하지 않으면 신원 없이 인증됨으로 오인할 수 있다. - [입력 유실 vs 실행 사이의 선택]: 올바른 설계는 "미인증 중 도착한 명령"을 (버려서 입력을 잃거나) 임의 실행하는 대신, 인증 완료까지 보류(큐잉) 하거나 명시적으로 거부/절단해야 한다.
- 재현 조건: 지연을 숨기려 접속 직후 낙관적으로 명령을 보내는 정상 클라, 또는 인증을 건너뛰려는 악성 클라. 외부 인증 지연이 클수록 창이 커진다.
수정안
게임 명령은 인증 완료를 전제로만 처리한다. 미인증 중 도착분은 세션별로 순서를 지켜 보류했다가 인증 완료 후 순서대로 처리하고, 신원·상태 전이는 원자적으로(같은 실행 컨텍스트에서) 게시한다.
public void OnReceive(Session session, Packet msg)
{
if (msg.Type == MsgType.Auth)
{
if (session.State != SessionState.Connecting) return; // 중복 AUTH 방지
session.State = SessionState.Authenticating;
_auth.ValidateAsync(msg.Token).ContinueWith(t =>
{
EnqueueToSessionActor(session, () => // 세션 단위 직렬 실행
{
if (t.IsFaulted || t.Result == null) { Close(session); return; }
session.AccountId = t.Result.AccountId;
session.State = SessionState.Authenticated; // 신원 먼저, 그 다음 상태 — 같은 컨텍스트
foreach (var pending in session.Pending) _handler.Handle(session, pending);
session.Pending.Clear();
});
});
return;
}
// 게임 명령: 인증 전이면 보류, 인증 후에만 처리 (모두 세션 액터에서 직렬 처리)
EnqueueToSessionActor(session, () =>
{
if (session.State != SessionState.Authenticated) { session.Pending.Add(msg); return; }
_handler.Handle(session, msg);
});
}
더 나은 설계
- 세션 단위 액터/직렬 큐: 한 세션의 모든 이벤트(수신·인증 완료)를 단일 순서로 처리하면 상태 전이와 명령 처리 사이의 경합·가시성 문제가 원천적으로 사라진다(문제 22 의 수신 직렬화와 연결).
- 보류 큐 상한: 미인증 보류 큐에 개수/바이트 상한과 핸드셰이크 타임아웃을 둬 slowloris 류 자원 고갈을 막는다(문제 24 와 연결).
- 명시적 상태 머신: 허용 메시지 집합을 상태별로 정의(Connecting 에선 AUTH 만 허용)해, 상태 위반 패킷은 거부·절단한다.
- 트레이드오프: 보류는 정상 클라의 낙관적 입력을 살리지만 메모리·지연을 늘린다. 즉시 거부는 단순하지만 접속 직후 입력을 잃는다 — 게임 특성에 맞게 선택한다.
면접 포인트
- 상태 머신에서 "허용 메시지" 를 상태별로 강제하지 않으면 인증 우회가 생긴다는 점.
- 비동기 완료 콜백에서 여러 필드를 게시할 때의 순서·가시성 문제와, 세션 직렬화로 해결하는 방법.
- 미인증 입력을 보류할지 거부할지의 트레이드오프(입력 유실 vs 자원/지연).
해설 · C++
해설 — 인증 전 패킷의 미인증 처리 + 비동기 인증 콜백의 세션 use-after-free
난이도: 상
요약
(B) 는 세션 상태를 확인하지 않고 게임 명령을 즉시 처리해 인증 전 명령이 accountId==0 으로 실행된다.
(A) 는 비동기 인증 콜백이 원시 포인터 Session* 를 캡처하는데, 검증이 끝나기 전에 연결이
종료되어 세션이 파괴되면 콜백이 해제된 메모리에 accountId/state 를 쓰는 use-after-free 다.
문제점
- [상태 게이트 부재] 미인증 명령 실행:
(B)는session->state를 보지 않는다. 접속 직후 온 MOVE 가Connecting/Authenticating상태에서 처리되어, 기본 신원(accountId==0)으로 이동·아이템 사용이 적용된다. 인증 우회·권한 상승 경로가 된다. - [수명/UAF] 콜백의 원시 포인터 캡처:
validateAsync의 콜백은 다른 스레드에서 나중에 실행된다. 그 사이 클라가 끊겨 세션 객체가 소멸되면, 콜백의session->accountId = ...는 dangling 포인터 역참조다. 반대로 세션이 살아있어도 소멸 스레드와 콜백 스레드가 동시에 세션 필드를 만지면 데이터 레이스다. - [발행 순서·가시성]
(A)는 다른 스레드에서accountId후state를 순서 없이 쓴다. 수신 스레드가state==Authenticated를 본 시점에accountId가 보인다는 보장이 없다(동기화 부재). - 재현 조건: 접속 직후 명령을 낙관적으로 보내는 클라(미인증 실행), 또는 인증 지연 중 즉시 끊는 클라/스캐너(UAF). 외부 인증 지연이 클수록 창이 커진다.
수정안
세션을 shared_ptr 로 관리해 콜백이 소유 지분을 잡게 하고(수명 안전), 인증 완료·명령 처리를 세션
단위로 직렬화하며, 게임 명령은 인증 완료 상태에서만 처리한다.
#include <memory>
// onReceive(std::shared_ptr<Session> session, const Packet& msg)
void onReceive(std::shared_ptr<Session> session, const Packet& msg) {
if (msg.type == MsgType::Auth) {
if (session->state != SessionState::Connecting) return;
session->state = SessionState::Authenticating;
std::weak_ptr<Session> weak = session; // 순환/좀비 방지: weak 캡처
auth_.validateAsync(msg.token, [this, weak](AuthResult r) {
auto s = weak.lock();
if (!s) return; // 세션이 이미 사라졌으면 안전하게 무시
postToSessionActor(s, [this, s, r] { // 세션 단위 직렬 실행
if (!r.ok) { close(s); return; }
s->accountId = r.accountId;
s->state = SessionState::Authenticated;
for (auto& p : s->pending) handler_.handle(*s, p);
s->pending.clear();
});
});
return;
}
postToSessionActor(session, [this, session, msg] {
if (session->state != SessionState::Authenticated) { session->pending.push_back(msg); return; }
handler_.handle(*session, msg);
});
}
더 나은 설계
shared_ptr+weak_ptr캡처: 콜백이 세션 수명을 안전하게 다룬다. 지분을 오래 쥐면 좀비 세션이 남으므로weak_ptr::lock으로 "살아있을 때만" 접근한다.- 세션 단위 액터/직렬 큐: 수신·인증 완료·명령을 단일 순서로 처리해 가시성·경합을 제거한다.
- 상태 머신 강제 + 보류 큐 상한 + 핸드셰이크 타임아웃: 상태별 허용 메시지만 받고, 미인증 보류 큐에 상한을 둬 자원 고갈(slowloris)을 막는다.
- 트레이드오프:
shared_ptr는 안전하지만 참조계수 비용과 좀비 위험(weak 로 완화)이 있고, 세션 직렬화는 구현이 늘지만 정합성이 크게 단순해진다.
면접 포인트
- 비동기 콜백이 원시 포인터/
this를 캡처할 때의 UAF 와shared_ptr/weak_ptr로의 해결. - 상태 머신에서 상태별 허용 메시지를 강제하지 않으면 인증 우회가 생긴다는 점.
- 미인증 입력의 보류 vs 거부 트레이드오프와 세션 직렬화의 이점.