Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/314-outbox-header-json-roundtrip-corruption.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

92 lines
4.3 KiB
Plaintext

# 주제: 역슬래시로 끝나는 헤더 값이 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 ..."
# => 제어문자 케이스만 테스트한다. 역슬래시로 *끝나는* 값에 대한 왕복 테스트가 없다.