22. 선행 퀘스트 상태를 클라가 보낸 값으로 신뢰하는 상황 (서버 권위)

난이도 하 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 선행 퀘스트 상태를 클라가 보낸 값으로 신뢰하는 상황 (서버 권위)
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
//   - 퀘스트 Y 를 수락하려면 선행 퀘스트 X 를 완료해야 한다.
//   - 클라이언트는 퀘스트 수락 요청에 "내가 가진 퀘스트 상태"(이미 완료한 퀘스트 ID
//     목록, 현재 레벨 등)를 함께 담아 보낸다. UI 응답성을 위해 클라가 미리 계산한다.
//   - 서버는 이 요청을 받아 선행 조건을 검사하고, 통과하면 Y 를 "진행 중" 으로 만든다.
//   - 퀘스트 진행도(예: "늑대 10마리 처치") 역시 클라가 카운트를 올려 보고하고,
//     목표치에 도달하면 완료 보상을 요청한다.
//
// 요구사항:
//   - 선행 조건/레벨 제한은 서버가 보유한 권위 데이터로 검증해야 한다(클라 신뢰 금지).
//   - 진행도 카운트는 실제 게임 이벤트(서버가 판정한 처치)로만 증가해야 한다.
//   - 조작된 요청으로 선행 없이 퀘스트를 받거나, 가짜 진행도로 보상을 받을 수 없어야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용·오작동하는지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class QuestAcceptRequest        // 클라이언트가 보내는 요청(신뢰 불가)
{
    public long   PlayerId;
    public int    QuestId;             // 수락하려는 퀘스트
    public int[]  CompletedQuestIds;   // 클라가 "이미 완료했다" 고 주장하는 선행 목록
    public int    PlayerLevel;         // 클라가 보고한 레벨
}

public class QuestProgressReport       // 진행도 보고(신뢰 불가)
{
    public long PlayerId;
    public int  QuestId;
    public int  KillCount;             // 클라가 누적한 처치 수
}

public class QuestDef
{
    public int RequiredQuestId;        // 선행 퀘스트(0이면 없음)
    public int RequiredLevel;
    public int TargetKillCount;        // 완료에 필요한 처치 수
    public long RewardGold;
}

public class QuestManager
{
    private readonly Dictionary<int, QuestDef> _defs;
    private readonly Func<long, long> _grantGold;            // 보상 지급 콜백

    // 서버가 보유한 권위 데이터(실제로는 DB/캐시)
    private readonly Dictionary<long, HashSet<int>> _serverCompleted = new();  // 완료한 퀘스트
    private readonly Dictionary<long, int>          _serverLevel     = new();  // 실제 레벨
    private readonly Dictionary<(long, int), int>   _activeProgress  = new();  // 진행 중 카운트

    public QuestManager(Dictionary<int, QuestDef> defs, Func<long, long> grantGold)
    {
        _defs = defs; _grantGold = grantGold;
    }

    public bool AcceptQuest(QuestAcceptRequest req)
    {
        var def = _defs[req.QuestId];

        // (A) 선행 조건/레벨을 "요청에 담겨 온 값" 으로 검사한다
        bool prereqOk = def.RequiredQuestId == 0
                        || Array.IndexOf(req.CompletedQuestIds, def.RequiredQuestId) >= 0;
        if (!prereqOk) return false;
        if (req.PlayerLevel < def.RequiredLevel) return false;

        _activeProgress[(req.PlayerId, req.QuestId)] = 0;
        return true;
    }

    public void ReportProgress(QuestProgressReport rep)
    {
        // (B) 클라가 보고한 누적 처치 수를 그대로 진행도로 받아들인다
        _activeProgress[(rep.PlayerId, rep.QuestId)] = rep.KillCount;
    }

    public bool ClaimReward(long playerId, int questId)
    {
        var def = _defs[questId];
        if (!_activeProgress.TryGetValue((playerId, questId), out var count)) return false;

        if (count < def.TargetKillCount) return false;     // 목표 미달

        _grantGold(def.RewardGold);
        _activeProgress.Remove((playerId, questId));
        if (!_serverCompleted.TryGetValue(playerId, out var done))
        { done = new HashSet<int>(); _serverCompleted[playerId] = done; }
        done.Add(questId);
        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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