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>
99 lines
4.6 KiB
Markdown
99 lines
4.6 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: scriptable-detection-bypassed-by-a-bom
|
|
title: scriptable 콘텐츠 탐지가 BOM·NUL·주석으로 우회된다
|
|
topic: fileserver-state-and-fencing
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:scriptable-detection-bypassed-by-a-bom
|
|
evidenceCapturedOn: 2026-09-01
|
|
assets:
|
|
- key: scriptable-detection-bypassed-by-a-bom
|
|
file: ../../../final/evidence/rendered/scriptable-detection-bypassed-by-a-bom.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/scriptable-detection-bypassed-by-a-bom.txt
|
|
source:
|
|
- 원본 분석 절은 final/document.md#4-4 · analysis/08 §40 이다.
|
|
---
|
|
|
|
# scriptable 콘텐츠 탐지가 BOM·NUL·주석으로 우회된다
|
|
|
|
브라우저가 실행할 수 있는 콘텐츠를 탐지하는 정책이 접두사 시작 매칭을 쓴다. 마커 앞에 바이트가 하나라도 있으면 탐지되지 않고, 브라우저는 그런 파일도 실행한다.
|
|
|
|
## 관계
|
|
|
|
- **접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다**
|
|
이 사례에서 끌어낸 규칙이다.
|
|
- **sanitize가 아니라 reject가 기본이다**
|
|
이 정책이 따르는 기본 방침이다.
|
|
|
|
## 문제
|
|
|
|
정책의 의도는 명확하고 옳다. javadoc 이 그것을 적는다.
|
|
|
|
브라우저가 인라인으로 제공될 경우 실행할 콘텐츠를 막는다. 탐지는 주장된 타입이나 확장자가 아니라 콘텐츠에 대해 한다. 둘 다 공격자가 통제하기 때문이다. 명시적 안전 프로파일이 켜져 있지 않으면 실행 가능 콘텐츠는 게시되지 않고 격리된다.
|
|
|
|
마커 목록도 합리적이다. HTML 선언과 여는 태그들과 XML 선언과 엔티티 선언이다.
|
|
|
|
## 결론
|
|
|
|
매칭 방식이 접두사 시작이다.
|
|
|
|
앞의 1024 바이트를 읽고 그 안에서 마커를 찾는데, 마커가 콘텐츠의 시작에 있어야 한다.
|
|
|
|
브라우저는 그렇게 엄격하지 않다. 앞에 바이트가 있어도 콘텐츠를 스니핑해 실행한다.
|
|
|
|
그래서 우회가 여럿이다.
|
|
|
|
바이트 순서 표시를 앞에 붙이면 마커가 시작이 아니다
|
|
널 바이트를 앞에 넣어도 같다
|
|
주석이나 공백을 앞에 두어도 같다
|
|
|
|
실행 탐침이 이 우회들을 확인했다.
|
|
|
|
시그니처 검사와 스니핑 패턴 검사는 다른 문제다. 시그니처는 파일 형식이 정의상 특정 바이트로 시작하므로 시작 매칭이 맞다. 브라우저 스니핑은 형식 정의가 아니라 관용적 해석이므로 시작 매칭이 맞지 않는다.
|
|
|
|
같은 함수가 두 목적에 쓰이면 한쪽이 틀린다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
확인 방식 : 실행 탐침으로 우회 입력 확인
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
원문은 final/evidence/raw/146-fileserver-verification-security-audit-probes.txt 의 실행 탐침 블록에 있다.
|
|
|
|
1. 정책 클래스의 마커 목록과 접두사 길이를 확인한다.
|
|
2. 매칭이 시작 기준인지 포함 기준인지 확인한다.
|
|
3. 마커 앞에 바이트를 붙인 입력으로 탐지 결과를 확인한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
javadoc이 목적을 "Detection is on content, not on the claimed type or the extension, because both are attacker controlled"로 적는데, 구현은 1,024바이트 접두사를 소문자화·`stripLeading()`한 뒤 여섯 마커로 **시작하는지**만 본다.
|
|
|
|
## javadoc 의 목적과 구현의 판정
|
|
|
|
:::evidence key="scriptable-detection-bypassed-by-a-bom" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
|
:::
|
|
|
|
## hermetic probe 가 통과시킨 셋
|
|
|
|
UTF-8 BOM + `<html>` · 선행 HTML 주석 후 `<script>` · NUL 바이트 후 `<html>`. `String.stripLeading()`은 `Character.isWhitespace`만 제거하므로 BOM(U+FEFF)도 NUL도 지우지 않는다. 셋 다 브라우저는 HTML로 렌더링하고, BOM 접두 HTML은 여러 편집기의 기본 출력이다.
|
|
|
|
## 형제 검증기와의 대비가 판정을 굳힌다
|
|
|
|
`MediaTypeVerifier`의 매직바이트 선두 매칭은 시그니처의 정의가 파일 선두이므로 옳지만, scriptable 마커는 시그니처가 아니라 브라우저가 스니핑하는 패턴이다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
실제 브라우저가 각 우회 입력을 실행하는지 확인하지 않았다. 브라우저의 스니핑 동작은 명세와 구현이 모두 관여하므로 별도 확인이 필요하다.
|
|
|
|
안전 프로파일이 켜진 배포에서의 동작을 확인하지 않았다.
|
|
|
|
<!-- body:end -->
|