# 주제: 역슬래시로 끝나는 헤더 값이 outbox 헤더 맵 전체를 깨뜨린다 (런타임 재현)
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
# severity: P2
# 방법: 애플리케이션 소스를 수정하지 않고, 컴파일된 클래스에 jshell + 리플렉션으로
#       private static toJson/fromJson 을 직접 호출했다. 테스트 파일을 추가하지 않았다.

# ---- 원인 ----
# JdbcOutboxRepository.java:657-664
#   private static int findClosingQuote(String text, int from) {
#     for (int index = from; index < text.length(); index++) {
#       if (text.charAt(index) == '"' && text.charAt(index - 1) != '\\') {
#         return index;
#       }
#     }
#     return text.length();
#   }
# 닫는 따옴표 판정이 "바로 앞 글자가 역슬래시가 아니다" 뿐이다.
# 값이 역슬래시로 끝나면 escape 가 그것을 두 개로 늘리므로(`\\`),
# 닫는 따옴표 바로 앞 글자가 역슬래시가 되어 종료 지점을 놓친다.
#
# 대조: escape(...) 는 제어문자까지 처리한다(:637-655). 그 javadoc(:630-636)이
#   "Control characters are the part of the JSON grammar that is easy to forget and impossible to
#    get away with." 라고 쓴다. 같은 쌍에서 잊힌 두 번째 자리가 이 종료 판정이다.

# ---- 재현 ----
# command: jshell --class-path <compiled classes> (리플렉션으로 private static 호출)
# exit: 0
=== case 1: 평범한 값 ===
  in   = {x-a=plain}
  json = {"x-a":"plain"}
  out  = {x-a=plain}
  EQUAL? true

=== case 2: 값 안의 따옴표 ===
  in   = {x-a=he said "hi"}
  json = {"x-a":"he said \"hi\""}
  out  = {x-a=he said "hi"}
  EQUAL? true

=== case 3: 값이 역슬래시로 끝남 ===
  in   = {x-a=a\}
  json = {"x-a":"a\\"}
  out  = {x-a=a\"}
  EQUAL? false                      <-- 값이 a\ -> a\" 로 변형

=== case 4: 역슬래시로 끝나는 값 + 뒤에 헤더 하나 더 ===
  in   = {x-a=a\, x-b=second}
  json = {"x-a":"a\\","x-b":"second"}
  out  = {x-a=a\",, :=x-a, a\",=second}
  EQUAL? false                      <-- 맵 전체가 붕괴.
                                        키 ":" 와 키 "a\"," 가 생기고 x-b 는 사라진다.

=== case 5: 값 중간의 역슬래시 (대조군) ===
  in   = {x-a=a\b}
  json = {"x-a":"a\\b"}
  out  = {x-a=a\b}
  EQUAL? true                       <-- 중간 역슬래시는 정상. 끝일 때만 깨진다.

=== HeaderValue 가 역슬래시를 허용하는가 ===
  new HeaderValue("a\") -> OK, value=a\
# => 플랫폼 자신의 검증 타입이 이 입력을 통과시킨다.
#    HeaderValue 는 WireSafeText.require(...) 로 제어문자만 금지한다(HeaderValue.java:24).
#    역슬래시는 제어문자가 아니다.

# ---- 헤더 주입으로는 이어지지 않는다 (심각도 상한 확인) ----
# 예약 이름을 파싱 결과의 키로 밀어넣을 수 있는지 확인:
  json = {"x-evil":"v\\","msg.id":"00000000-0000-0000-0000-000000000000"}
  out  = {x-evil=v\",, :=x-evil, v\",=00000000-0000-0000-0000-000000000000}
    key=[x-evil]  reserved=false
    key=[:]       reserved=false
    key=[v\",]    reserved=false
# => 어긋남이 키/값 경계를 밀어내므로 msg.id 는 키가 아니라 *값* 이 된다. 예약 키는 생성되지 않았다.
# 또한 쓰기 경로가 애초에 막는다:
  new HeaderName("msg.id") -> OK
  MessageHeaders.application({msg.id}) -> REJECTED: message header is not allowed: msg.id
# => 데이터 손상 결함이며 헤더 주입 취약점은 아니다.

# ---- 영향 경로 ----
# JdbcOutboxRepository.read(...) -> fromJson(headers 컬럼)
#   -> OutboxRecord.headers()
#   -> OutboxEnvelopeFactory.toEnvelope(...):61-79  (예약 이름이면 throw, 아니면 그대로 통과)
#   -> MessageHeaders.platform(headers) -> 브로커로 발행
# case 4 의 깨진 키들은 예약 이름이 아니므로 throw 되지 않고 그대로 나간다.
# CDC 경로(DebeziumOutboxRecordMapper:59-61)는 예약 이름을 throw 대신 제거하므로
# 마찬가지로 조용히 통과한다.

# ---- 테스트가 덮지 않는다 ----
# command: grep -rn "escape|backslash" messaging-outbox-jdbc-postgresql/src/test
#   OutboxPostgresIT.java:547  "// A tab and a newline in a header value. Only backslash and quote
#                               were escaped, so ..."
# => 제어문자 케이스만 테스트한다. 역슬래시로 *끝나는* 값에 대한 왕복 테스트가 없다.
