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++
// ============================================================================
// [코드리뷰 문제] C++ - 선행 퀘스트 상태를 클라가 보낸 값으로 신뢰하는 상황 (서버 권위)
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
// - 퀘스트 Y 를 수락하려면 선행 퀘스트 X 를 완료해야 한다.
// - 클라이언트는 수락 요청에 "내가 가진 퀘스트 상태"(완료한 퀘스트 ID 목록, 레벨)를
// 함께 담아 보낸다. UI 응답성을 위해 클라가 미리 계산한다.
// - 서버는 이 요청을 받아 선행 조건을 검사하고 통과하면 Y 를 "진행 중" 으로 만든다.
// - 진행도(예: "늑대 10마리")도 클라가 카운트를 올려 보고하고, 목표치에 도달하면
// 완료 보상을 요청한다.
//
// 요구사항:
// - 선행/레벨 제한은 서버 보유 권위 데이터로만 검증해야 한다(클라 신뢰 금지).
// - 진행도는 서버가 판정한 실제 게임 이벤트로만 증가해야 한다.
// - 조작된 요청으로 선행 없이 퀘스트를 받거나 가짜 진행도로 보상받을 수 없어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용·오작동하는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
#include <functional>
struct QuestAcceptRequest { // 클라가 보내는 요청(신뢰 불가)
std::int64_t playerId;
int questId;
std::vector<int> completedQuestIds; // 클라가 "완료했다" 주장하는 선행 목록
int playerLevel; // 클라가 보고한 레벨
};
struct QuestDef {
int requiredQuestId = 0; // 0이면 선행 없음
int requiredLevel = 1;
int targetKillCount = 1;
std::int64_t rewardGold = 0;
};
class QuestManager {
public:
QuestManager(std::unordered_map<int, QuestDef> defs,
std::function<void(std::int64_t)> grantGold)
: defs_(std::move(defs)), grantGold_(std::move(grantGold)) {}
bool AcceptQuest(const QuestAcceptRequest& req) {
const QuestDef& def = defs_.at(req.questId);
// (A) 선행/레벨을 "요청에 담겨 온 값" 으로 검사한다
bool prereqOk = def.requiredQuestId == 0
|| std::find(req.completedQuestIds.begin(), req.completedQuestIds.end(),
def.requiredQuestId) != req.completedQuestIds.end();
if (!prereqOk) return false;
if (req.playerLevel < def.requiredLevel) return false;
active_[{req.playerId, req.questId}] = 0;
return true;
}
// 진행도 보고(클라가 누적 카운트를 보냄)
void ReportProgress(std::int64_t playerId, int questId, int killCount) {
// (B) 클라가 보고한 누적 처치 수를 그대로 진행도로 받아들인다
active_[{playerId, questId}] = killCount;
}
bool ClaimReward(std::int64_t playerId, int questId) {
const QuestDef& def = defs_.at(questId);
auto it = active_.find({playerId, questId});
if (it == active_.end()) return false;
if (it->second < def.targetKillCount) return false;
grantGold_(def.rewardGold);
active_.erase(it);
serverCompleted_[playerId].insert(questId);
return true;
}
private:
struct PairHash {
std::size_t operator()(const std::pair<std::int64_t,int>& p) const {
return std::hash<std::int64_t>()(p.first) ^ (std::hash<int>()(p.second) << 1);
}
};
std::unordered_map<int, QuestDef> defs_;
std::function<void(std::int64_t)> grantGold_;
// 서버가 보유한 권위 데이터(실제로는 DB/캐시)
std::unordered_map<std::int64_t, std::unordered_set<int>> serverCompleted_;
std::unordered_map<std::int64_t, int> serverLevel_;
std::unordered_map<std::pair<std::int64_t,int>, int, PairHash> active_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.