← 문제로

38. UTF-8 문자열 필드의 인코딩 유효성 검증 부재 (C#)

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

해설 — UTF-8 문자열 필드의 인코딩 유효성 검증 부재 (C#)

난이도: 중상

요약

DecodeString 은 길이 경계만 신뢰하고 바이트가 유효한 UTF-8 인지를 검증하지 않는다. Encoding.UTF8.GetString 은 기본적으로 잘못된 바이트를 예외 없이 U+FFFD(대체 문자)로 치환해 통과시킨다. 그 결과 오버롱 인코딩·불완전 멀티바이트·제어문자·양방향(Bidi) 제어·제로폭 문자가 그대로 저장·재전송돼, 다운스트림에서 스푸핑(동형 문자 닉네임)·로그 오염·필터 우회·다른 파서(다른 클라/DB)의 처리 붕괴를 일으킨다.

문제점

  • [입력 검증/인코딩] 바이트 유효성·문자 허용목록 미검증 → 오염 문자열 전파

    • 증상: (B) 는 어떤 바이트열이든 문자열로 만든다. 잘못된 시퀀스는 U+FFFD 로 조용히 바뀌어 "복원 성공"처럼 보이고, 유효하지만 위험한 코드포인트(제어문자, 양방향 오버라이드 U+202E, 제로폭 U+200B, 동형 이체자)는 그대로 통과한다.
    • 재현: 닉네임에 U+202E(RTL override)를 넣어 표시를 뒤집거나, 제로폭 문자로 금지어 필터를 우회하거나, 제어문자로 로그/콘솔을 오염. 잘못된/오버롱 시퀀스는 느슨한 디코더나 바이트 단위 필터를 우회할 수 있고, 기본 Encoding.UTF8 은 이를 예외 대신 U+FFFD 로 조용히 치환해 "복원 성공"처럼 보이게 만든다.
    • 근본원인: "길이가 맞다 ≠ 내용이 안전하다". 인코딩 유효성(엄격 디코딩)과 도메인 허용 문자 정책이 둘 다 없다.
  • [다운스트림 불일치] 관대한 디코딩 → 파서마다 다르게 해석

    • 증상: 서버는 U+FFFD 로 치환했는데 다른 클라이언트/DB 는 다르게 처리 → 표시 불일치, 저장 실패, 인젝션.
    • 근본원인: 관대(replace) 디코딩은 파서 간 해석 차이를 낳는다. 신뢰 경계에서는 엄격 디코딩이 원칙.

수정안

엄격 디코딩(잘못된 바이트는 예외)으로 유효성을 강제하고, 도메인 정책(정규화·허용 문자·길이)을 적용한다.

private static readonly Encoding StrictUtf8 =
    new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

public string DecodeString(ReadOnlySpan<byte> frame, int offset, int byteLen)
{
    ReadOnlySpan<byte> bytes = frame.Slice(offset, byteLen);
    string s;
    try { s = StrictUtf8.GetString(bytes); }             // 잘못된 UTF-8 → 예외로 거부
    catch (DecoderFallbackException) { throw new InvalidDataException("invalid utf-8"); }

    s = s.Normalize(NormalizationForm.FormC);            // 유니코드 정규화
    foreach (char c in s)
        if (char.IsControl(c) || IsDisallowed(c))        // 제어/Bidi/제로폭 등 정책 차단
            throw new InvalidDataException("disallowed char");
    if (s.Length > MaxChars) throw new InvalidDataException("too long");
    return s;
}

더 나은 설계

  • 닉네임/표시명 등 정체성 문자열은 더 엄격하게: 스크립트 혼용 제한(라틴+키릴 혼용 차단), confusable(동형 이체) 검사, 정규화 후 유일성 검사로 스푸핑을 막는다.
  • 채팅 본문은 표시 계층에서 제어/Bidi 문자를 무력화하고, 저장은 원문+정규화본을 분리 보관해 검색·필터 일관성 확보.
  • 경계 원칙: 신뢰할 수 없는 입력은 "엄격 파싱 → 정규화 → 허용목록 검증 → 저장" 순서를 항상 거친다(디코딩을 관대하게 하지 말 것).

면접 포인트

  • "길이 검증 통과 = 안전"이 아니다. 인코딩 유효성(엄격 UTF-8)과 코드포인트 정책(제어/Bidi/제로폭/동형)이 별개의 방어선이다.
  • Encoding.UTF8 기본이 예외 대신 U+FFFD 치환이라는 점을 알고, 신뢰 경계에서는 throwOnInvalidBytes: true 를 써야 함을 설명할 수 있어야 한다.
  • 오버롱 인코딩이 왜 필터 우회에 쓰이는지, 유니코드 정규화가 왜 유일성/필터 일관성에 필요한지 말할 수 있어야 한다.

이 항목은 입력 검증/방어 코드 설계 학습이 목적이며 실제 공격 재현이 아니다.