24. 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황
난이도 하해설 — 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황
난이도: 하
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
세 경로(Trade/AttachToMail/ListOnAuction) 모두 서버 권위 데이터(Bound/Tradable)로
거래 가능 여부를 판정하지 않는다. (A) Trade 는 거래 가능 여부를 클라가 보낸
ClientSaysTradable 값으로 판정한다 → 클라가 항상 true 로 보내면 귀속템도 넘어간다.
(B) AttachToMail 은 아무 검증 없이 우편으로 이전한다. (C) ListOnAuction 은 소유자 확인도,
귀속/거래불가 검증도 없어 남의 아이템·귀속템을 경매에 올린다. 정답 한 줄: 거래 가능 여부는
항상 서버 권위 아이템 데이터(!Bound && Tradable)로 판정하고, 모든 이전 경로에서 소유권과
귀속/거래불가를 공통 검증한 뒤에만 이동시킨다.
변별: 본 문제는 "어떤 아이템이 이전 자격이 있는가(귀속/거래불가/소유권)" 의 서버 권위 검증이다. problem3(직거래의 복제·에스크로 원자성), problem6(우편 수령 중복 지급), problem4(경매 동시 입찰 race) 와 달리, 여기서는 이동 자격(eligibility) 판정이 핵심이다.
문제점
(A) 거래 가능 여부를 클라 값으로 판정 — 서버 권위 위반 (보안/치팅) ★간판
- 분류 태그: trust-client / authority-bypass.
- 증상:
if (!req.ClientSaysTradable) return false;는 클라가 보낸 플래그에 의존한다. 공격자가 귀속·거래불가 아이템에 대해 패킷의ClientSaysTradable=true로 위조하면 그대로 통과 → 귀속템이 다른 플레이어에게 이전된다(현금거래·복제 어뷰징의 입구). - 재현조건: 변조된 거래 패킷 1개. (값 자체가 클라 제어이므로 항상 재현 가능)
- 근본 원인: 거래 가능 여부의 권위가 클라에 있다. 서버는
item.Bound,item.Tradable를 갖고 있으면서 쓰지 않는다.
(B) 우편 첨부에 귀속/거래불가 검증 없음 — 우회 경로 (보안/정합)
- 증상:
AttachToMail은Bound/Tradable을 보지 않고 무조건OwnerId를 바꾼다. 설령Trade를 막아도 우편이라는 다른 경로로 귀속템이 새어나간다. - 근본 원인: 자격 검증이 경로마다 흩어져 있고(우편엔 아예 없음), 공통 게이트가 없다.
(C) 경매 출품에 소유권/귀속 검증 없음 — 권한·자격 누락 (보안)
- 증상:
ListOnAuction(sellerId, itemId)가sellerId와item.OwnerId의 일치도 확인하지 않고, 귀속/거래불가도 보지 않는다. 남의 아이템 itemId 만 알면 경매에 올릴 수 있고, 귀속템도 출품된다. - 근본 원인: 소유권(authorization)과 자격(eligibility) 검증이 둘 다 빠졌다.
(공통) FromPlayer/소유권 검증 부재 — 권한
Trade도item.OwnerId == req.FromPlayer를 확인하지 않는다. 요청자가 실제 소유자인지 서버가 확인하지 않으면 타인 아이템을 이전시킬 수 있다.
수정안
핵심: ① 이전 자격을 서버 권위 데이터로만 판정, ② 소유권 확인, ③ 모든 경로가 하나의 공통 게이트를 통과.
public class ItemTransferService
{
private readonly Dictionary<long, Item> _items;
private readonly Func<long, long, bool> _giveToPlayer;
// 공통 게이트: 소유권 + 이전 자격(서버 권위)
private bool CanTransfer(long requesterId, long itemId, out Item item)
{
item = null;
if (!_items.TryGetValue(itemId, out var it)) return false;
if (it.OwnerId != requesterId) return false; // 소유권(권한) 확인
if (it.Bound) return false; // 귀속템 이전 금지
if (!it.Tradable) return false; // 거래불가 이전 금지
item = it;
return true;
}
public bool Trade(TradeRequest req)
{
// 클라가 보낸 ClientSaysTradable 은 무시 — 서버 권위로만 판정
if (!CanTransfer(req.FromPlayer, req.ItemId, out var item)) return false;
item.OwnerId = req.ToPlayer;
return _giveToPlayer(req.ToPlayer, req.ItemId);
}
public bool AttachToMail(long fromPlayer, long toPlayer, long itemId)
{
if (!CanTransfer(fromPlayer, itemId, out var item)) return false;
item.OwnerId = toPlayer;
return _giveToPlayer(toPlayer, itemId);
}
public bool ListOnAuction(long sellerId, long itemId)
{
if (!CanTransfer(sellerId, itemId, out var item)) return false;
item.OwnerId = 0; // 경매장 보관
return true;
}
}
포인트
- 클라가 보낸
ClientSaysTradable은 버리고,item.Bound/item.Tradable로만 판정. CanTransfer한 곳에 소유권 + 귀속 + 거래불가를 모아 모든 이전 경로가 같은 규칙을 탄다.- NPC 상점 판매처럼 정책이 다른 경로는 별도 규칙(예:
SellableToNpc)으로 분리하되, 역시 서버 권위 플래그로 판정.
더 나은 설계 (+트레이드오프)
- 이전 자격을 타입/정책 테이블로 명세:
(귀속상태, Tradable, 경로)→ 허용 여부를 데이터로 선언하고 모든 경로가 같은 표를 참조. 트레이드오프: 정책 표 관리 비용, 하지만 "경로마다 빠뜨림"을 구조적으로 차단. - 자격 검증을 도메인 모델(Item)에 위임:
item.CanBeTransferredBy(requester, channel)처럼 캡슐화 → 호출부가 우회 불가. 트레이드오프: 모델 비대화 주의. - 감사 로그/이상탐지: 귀속템 이전 시도(거부 포함)를 로깅해 어뷰징 패턴 탐지.
- 이전은 원자 트랜잭션으로: 자격 검증 후 (출발 인벤 제거 + 도착 인벤 추가 + 소유권 변경) 을 한 트랜잭션으로(복제·유실 방지 — problem3 와 연계).
면접 포인트 (예상 질문)
- "거래 가능 여부" 를 클라가 보낸 값으로 판정하면 어떤 어뷰징이 가능한가? 서버 권위 원칙을 한 문장으로 말하라.
Trade만 막고AttachToMail·ListOnAuction을 빠뜨리면 왜 무의미한가? 어떻게 구조적으로 모든 경로를 한 게이트로 모으겠는가?- 소유권(authorization) 검증과 자격(eligibility) 검증은 어떻게 다른가? 둘 다 필요한 이유는?
해설 — 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황 (C++)
난이도: 하
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
세 경로(trade/attachToMail/listOnAuction) 모두 서버 권위 데이터(bound/tradable)로
이전 자격을 판정하지 않는다. (A) trade 는 거래 가능 여부를 클라가 보낸
clientSaysTradable 로 판정 → 위조하면 귀속템도 이전된다. (B) attachToMail 은 무검증으로
우편 이전. (C) listOnAuction 은 소유권 확인도, 귀속/거래불가 검증도 없어 남의 아이템·
귀속템을 출품한다. 정답 한 줄: 거래 가능 여부는 서버 권위(!bound && tradable)로만 판정하고,
모든 이전 경로가 소유권+귀속+거래불가를 공통 게이트에서 검증한 뒤에만 이동시킨다.
변별: 본 문제는 "아이템의 이전 자격(귀속/거래불가/소유권)" 서버 권위 검증이다. problem3(직거래 복제·에스크로), problem6(우편 중복 수령), problem4(경매 동시 입찰)와 달리 이동 자격 판정이 핵심이다.
문제점
(A) 거래 가능 여부를 클라 값으로 판정 — 서버 권위 위반 (보안/치팅) ★간판
- 분류 태그: trust-client / authority-bypass.
- 증상:
if (!req.clientSaysTradable) return false;는 클라 제어 플래그에 의존한다. 귀속·거래불가 아이템도clientSaysTradable=true로 위조하면 통과 → 다른 플레이어에게 이전. - 재현조건: 변조된 거래 패킷 1개(값이 클라 제어라 항상 재현).
- 근본 원인: 자격 판정의 권위가 클라에 있다. 서버는
item.bound/item.tradable를 보유하면서 쓰지 않는다.
(B) 우편 첨부에 자격 검증 없음 — 우회 경로 (보안/정합)
- 증상:
attachToMail은bound/tradable를 보지 않고 무조건ownerId를 바꾼다.trade를 막아도 우편으로 귀속템이 샌다. - 근본 원인: 검증이 경로마다 흩어져 있고 우편엔 아예 없음(공통 게이트 부재).
(C) 경매 출품에 소유권/귀속 검증 없음 — 권한·자격 누락 (보안)
- 증상:
listOnAuction(sellerId, itemId)가sellerId == item.ownerId도, 귀속/거래불가도 확인하지 않는다. 남의 itemId 만 알면 출품 가능, 귀속템도 출품. - 근본 원인: 소유권(authorization) + 자격(eligibility) 검증 둘 다 빠짐.
(공통) 소유권 검증 부재 — 권한
trade도item.ownerId == req.fromPlayer를 확인하지 않아 타인 아이템 이전이 가능하다.
수정안
핵심: ① 자격은 서버 권위 데이터로만, ② 소유권 확인, ③ 모든 경로가 하나의 공통 게이트.
class ItemTransferService {
public:
// ... 생성자 동일 ...
// 공통 게이트: 소유권 + 이전 자격(서버 권위). 통과 시 Item* 반환, 아니면 nullptr.
Item* canTransfer(int64_t requesterId, int64_t itemId) {
auto it = items_.find(itemId);
if (it == items_.end()) return nullptr;
Item& item = it->second;
if (item.ownerId != requesterId) return nullptr; // 소유권(권한)
if (item.bound) return nullptr; // 귀속템 금지
if (!item.tradable) return nullptr; // 거래불가 금지
return &item;
}
bool trade(const TradeRequest& req) {
// clientSaysTradable 은 무시 — 서버 권위로만 판정
Item* item = canTransfer(req.fromPlayer, req.itemId);
if (!item) return false;
item->ownerId = req.toPlayer;
return giveToPlayer_(req.toPlayer, req.itemId);
}
bool attachToMail(int64_t fromPlayer, int64_t toPlayer, int64_t itemId) {
Item* item = canTransfer(fromPlayer, itemId);
if (!item) return false;
item->ownerId = toPlayer;
return giveToPlayer_(toPlayer, itemId);
}
bool listOnAuction(int64_t sellerId, int64_t itemId) {
Item* item = canTransfer(sellerId, itemId);
if (!item) return false;
item->ownerId = 0; // 경매장 보관
return true;
}
};
포인트
canTransfer가 소유권+귀속+거래불가를 한 곳에서 강제 → 모든 경로가 같은 규칙.- 반환
Item*의 수명은items_맵(노드 기반unordered_map)에 종속 — 같은 콜 안에서만 쓰고, 맵에 동시 삽입/삭제가 없을 때만 안전(멀티스레드면 락 필요, 본 문제 범위 밖). - 클라
clientSaysTradable은 폐기.
더 나은 설계 (+트레이드오프)
- 자격 정책 테이블:
(귀속, tradable, 경로)→ 허용 여부를 데이터로 선언, 모든 경로가 참조. 트레이드오프: 표 관리 비용 ↔ "경로 누락"의 구조적 차단. - 도메인 모델에 캡슐화:
item.canBeTransferredBy(requester, channel)로 묶어 호출부 우회 불가. 트레이드오프: 모델 비대화 주의. - 감사 로그/이상탐지: 귀속템 이전 시도(거부 포함) 로깅으로 어뷰징 탐지.
- 이전을 원자 트랜잭션으로: (출발 제거 + 도착 추가 + 소유권 변경) 원자화(복제 방지, problem3 연계).
면접 포인트 (예상 질문)
- 거래 가능 여부를 클라 값으로 판정하면 어떤 어뷰징이 가능한가? 서버 권위 원칙은?
trade만 막고 우편/경매를 빠뜨리면 왜 무의미한가? 공통 게이트로 모으는 설계는?unordered_map::find가 돌려준Item&/Item*의 수명은 무엇에 묶이나? 동시에 맵에 삽입/삭제가 일어나면 왜 위험한가(rehash/erase)?