24. 중첩 컨테이너 재귀 역직렬화 (깊이 제한 부재 / 노드 예산 부재)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 중첩 가능한 컨테이너 메시지의 재귀 역직렬화
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/직렬화 - 서버-클라/서버-서버):
// - 게임 메시지는 "값(Value)"의 트리다. 값은 정수(INT)이거나, 다른 값들을 담는
// 컨테이너(LIST)다. LIST 안에 LIST 가 또 들어갈 수 있어 임의 깊이로 중첩된다.
// - 와이어 포맷: [tag(1바이트)] 뒤에 INT 면 [int32], LIST 면 [count(uint32)] + count개의 값.
// - 수신측은 재귀적으로 파싱해 Value 트리를 만든 뒤 게임 로직에 넘긴다.
//
// 상황:
// - 신뢰할 수 없는 송신자(클라 또는 다른 존 서버)가 보낸 바이트열을 ParseValue 가 파싱한다.
// - 정상 메시지는 보통 2~3단계 중첩이지만, 입력은 공격자가 자유롭게 구성할 수 있다.
//
// 과제:
// 이 파서가 적대적 입력에 대해 어떻게 동작하는지 분석하라. 특히 (A) 지점의 재귀 호출과
// (B) 에서 읽는 count 가 입력에 따라 무엇을 좌우하는지, 그리고 서버 프로세스가
// 어떤 영향을 받는지 설명하라. 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public enum Tag : byte { Int = 1, List = 2 }
public sealed class Value
{
public Tag Tag;
public int IntVal;
public List<Value> Children = new();
}
public ref struct ByteReader
{
private readonly ReadOnlySpan<byte> _buf;
private int _pos;
public ByteReader(ReadOnlySpan<byte> buf) { _buf = buf; _pos = 0; }
public byte ReadU8()
{
if (_pos + 1 > _buf.Length) throw new FormatException("truncated");
return _buf[_pos++];
}
public int ReadI32()
{
if (_pos + 4 > _buf.Length) throw new FormatException("truncated");
int v = BitConverter.ToInt32(_buf.Slice(_pos, 4));
_pos += 4;
return v;
}
public uint ReadU32() => unchecked((uint)ReadI32());
}
public static class MessageParser
{
// 재귀 파서. 신뢰 불가 입력에서 호출된다.
public static Value ParseValue(ref ByteReader r)
{
var v = new Value { Tag = (Tag)r.ReadU8() };
if (v.Tag == Tag.Int)
{
v.IntVal = r.ReadI32();
return v;
}
// Tag.List
uint count = r.ReadU32(); // (B) 자식 개수
v.Children = new List<Value>((int)count); // (C) 미리 확보
for (uint i = 0; i < count; i++)
v.Children.Add(ParseValue(ref r)); // (A) 자식마다 재귀
return v;
}
public static Value ParsePacket(ReadOnlySpan<byte> data)
{
var r = new ByteReader(data);
return ParseValue(ref r);
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 중첩 가능한 컨테이너 메시지의 재귀 역직렬화
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/직렬화 - 서버-클라/서버-서버):
// - 게임 메시지는 "값(Value)"의 트리다. 값은 정수(INT)이거나, 다른 값들을 담는
// 컨테이너(LIST)다. LIST 안에 LIST 가 또 들어갈 수 있어 임의 깊이로 중첩된다.
// - 와이어 포맷: [tag(1바이트)] 뒤에 INT 면 [int32], LIST 면 [count(uint32)] + count개의 값.
// - 수신측은 재귀적으로 파싱해 Value 트리를 만든 뒤 게임 로직에 넘긴다.
//
// 상황:
// - 신뢰할 수 없는 송신자(클라 또는 다른 존 서버)가 보낸 바이트열을 parseValue 가 파싱한다.
// - 정상 메시지는 보통 2~3단계 중첩이지만, 입력은 공격자가 자유롭게 구성할 수 있다.
//
// 과제:
// 이 파서가 적대적 입력에 대해 어떻게 동작하는지 분석하라. 특히 (A) 지점의 재귀 호출과
// (B) 에서 읽는 count 가 입력에 따라 무엇을 좌우하는지, 그리고 프로세스 자원(스택/힙/CPU)이
// 어떤 영향을 받는지 설명하라. 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <cstring>
#include <vector>
#include <memory>
#include <stdexcept>
enum Tag : uint8_t { TAG_INT = 1, TAG_LIST = 2 };
struct Value {
Tag tag;
int32_t intVal = 0;
std::vector<std::unique_ptr<Value>> children;
};
class ByteReader {
public:
ByteReader(const uint8_t* p, size_t n) : p_(p), n_(n) {}
uint8_t readU8() { ensure(1); return p_[pos_++]; }
uint32_t readU32() {
ensure(4);
uint32_t v; std::memcpy(&v, p_ + pos_, 4); pos_ += 4;
return v; // (엔디안은 논외로 둔다)
}
int32_t readI32() { return static_cast<int32_t>(readU32()); }
private:
void ensure(size_t k) { if (pos_ + k > n_) throw std::runtime_error("truncated"); }
const uint8_t* p_; size_t n_; size_t pos_ = 0;
};
// 재귀 파서. 신뢰 불가 입력에서 호출된다.
std::unique_ptr<Value> parseValue(ByteReader& r) {
auto v = std::make_unique<Value>();
v->tag = static_cast<Tag>(r.readU8());
if (v->tag == TAG_INT) {
v->intVal = r.readI32();
return v;
}
// TAG_LIST
uint32_t count = r.readU32(); // (B) 자식 개수
v->children.reserve(count); // (C) 미리 확보
for (uint32_t i = 0; i < count; ++i) {
v->children.push_back(parseValue(r)); // (A) 자식마다 재귀
}
return v;
}
std::unique_ptr<Value> parsePacket(const uint8_t* data, size_t len) {
ByteReader r(data, len);
return parseValue(r);
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.