← 문제로

24. 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황

난이도 하
내 리뷰 · C#
해설 · C#

해설 — 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황

난이도: 하

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

세 경로(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) 우편 첨부에 귀속/거래불가 검증 없음 — 우회 경로 (보안/정합)

  • 증상: AttachToMailBound/Tradable 을 보지 않고 무조건 OwnerId 를 바꾼다. 설령 Trade 를 막아도 우편이라는 다른 경로로 귀속템이 새어나간다.
  • 근본 원인: 자격 검증이 경로마다 흩어져 있고(우편엔 아예 없음), 공통 게이트가 없다.

(C) 경매 출품에 소유권/귀속 검증 없음 — 권한·자격 누락 (보안)

  • 증상: ListOnAuction(sellerId, itemId)sellerIditem.OwnerId 의 일치도 확인하지 않고, 귀속/거래불가도 보지 않는다. 남의 아이템 itemId 만 알면 경매에 올릴 수 있고, 귀속템도 출품된다.
  • 근본 원인: 소유권(authorization)과 자격(eligibility) 검증이 둘 다 빠졌다.

(공통) FromPlayer/소유권 검증 부재 — 권한

  • Tradeitem.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)으로 분리하되, 역시 서버 권위 플래그로 판정.

더 나은 설계 (+트레이드오프)

  1. 이전 자격을 타입/정책 테이블로 명세: (귀속상태, Tradable, 경로) → 허용 여부를 데이터로 선언하고 모든 경로가 같은 표를 참조. 트레이드오프: 정책 표 관리 비용, 하지만 "경로마다 빠뜨림"을 구조적으로 차단.
  2. 자격 검증을 도메인 모델(Item)에 위임: item.CanBeTransferredBy(requester, channel) 처럼 캡슐화 → 호출부가 우회 불가. 트레이드오프: 모델 비대화 주의.
  3. 감사 로그/이상탐지: 귀속템 이전 시도(거부 포함)를 로깅해 어뷰징 패턴 탐지.
  4. 이전은 원자 트랜잭션으로: 자격 검증 후 (출발 인벤 제거 + 도착 인벤 추가 + 소유권 변경) 을 한 트랜잭션으로(복제·유실 방지 — problem3 와 연계).

면접 포인트 (예상 질문)

  1. "거래 가능 여부" 를 클라가 보낸 값으로 판정하면 어떤 어뷰징이 가능한가? 서버 권위 원칙을 한 문장으로 말하라.
  2. Trade 만 막고 AttachToMail·ListOnAuction 을 빠뜨리면 왜 무의미한가? 어떻게 구조적으로 모든 경로를 한 게이트로 모으겠는가?
  3. 소유권(authorization) 검증과 자격(eligibility) 검증은 어떻게 다른가? 둘 다 필요한 이유는?