8. C# 재접속 토큰 검증 / 세션 탈취 방지

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 재접속 토큰 검증 / 세션 탈취(hijacking) 방지
// ----------------------------------------------------------------------------
// 시나리오:
//   - 모바일 게임. 네트워크가 자주 끊겨 "재접속(resume)"이 빈번하다.
//   - 최초 로그인 성공 시 서버가 ResumeToken 을 발급해 클라에 준다.
//   - 끊긴 세션은 ResumeWindow(예: 60초) 동안 보관되고, 클라가 같은 토큰을
//     들고 재접속하면 기존 세션(인벤토리/위치/매칭 상태)을 그대로 이어받는다.
//   - 재접속 요청은 여러 IO 스레드에서 동시에 들어올 수 있다.
//
// 요구사항:
//   - 토큰을 모르는 제3자가 남의 세션을 가로채면 안 된다(세션 탈취 방지).
//   - 토큰은 ResumeWindow 안에서만, 그리고 한 번의 재접속에만 유효해야 한다.
//   - 같은 토큰으로 동시에 두 클라가 들어오는 race 에서도 하나만 성공해야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;
using System.Net;

public sealed class PlayerSession
{
    public long AccountId;
    public string ResumeToken;          // (A)
    public IPAddress ClientIp;
    public DateTime DisconnectedAt;
    public bool Connected = true;

    public void Rebind(System.Net.Sockets.Socket newSocket)
    {
        // 새 소켓으로 교체하고 세션 상태 이어받기 ...
        Connected = true;
    }
}

public sealed class ResumeService
{
    // 끊김 보관소: token -> 세션
    private readonly Dictionary<string, PlayerSession> _suspended =
        new Dictionary<string, PlayerSession>();
    private readonly object _lock = new object();
    private static readonly TimeSpan ResumeWindow = TimeSpan.FromSeconds(60);

    // 최초 로그인 성공 시 토큰 발급
    public string IssueToken(PlayerSession s)
    {
        // 짧고 예측하기 쉬운 토큰 (계정 기반)  (B)
        string token = "R" + s.AccountId.ToString();
        s.ResumeToken = token;
        return token;
    }

    // 끊김 시 호출: 보관소에 넣어 둔다
    public void Suspend(PlayerSession s)
    {
        s.Connected = false;
        s.DisconnectedAt = DateTime.UtcNow;
        lock (_lock)
        {
            _suspended[s.ResumeToken] = s;
        }
    }

    // 재접속 요청 처리: 클라가 보낸 token 으로 기존 세션을 찾아 이어받는다.
    public PlayerSession TryResume(string token, System.Net.Sockets.Socket newSocket,
                                   IPAddress fromIp)
    {
        PlayerSession s;
        lock (_lock)
        {
            if (!_suspended.TryGetValue(token, out s))   // (C)
                return null;

            // 만료 검사
            if (DateTime.UtcNow - s.DisconnectedAt > ResumeWindow)   // (D)
            {
                _suspended.Remove(token);
                return null;
            }
        }

        // 토큰이 맞으면 세션을 넘겨준다. (E)
        s.ClientIp = fromIp;
        s.Rebind(newSocket);
        return s;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.