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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+190
@@ -0,0 +1,190 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a08-f005-transferbufferpool-maxborrowedbytes
|
||||
title: 부재로 판정된 결합이 원본 증거의 출력 안에 있다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a08-f005-transferbufferpool-maxborrowedbytes
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a08-f005-transferbufferpool-maxborrowedbytes
|
||||
file: ../../../final/evidence/rendered/a08-f005-transferbufferpool-maxborrowedbytes.svg
|
||||
- key: a08-f005-transferbufferpool-maxborrowedbytes-peak
|
||||
file: ../../../final/evidence/rendered/a08-f005-transferbufferpool-maxborrowedbytes-peak.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes.txt
|
||||
- ../../../final/evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes-peak.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/08-adapter-outbound-fileserver.md#L410 이다. 등급은 P3 이다. 자바독의 문장, 계측이 옳게 동작한다는 확인, 그리고 경계 성질이 다른 방식으로도 검증된다는 서술이 그 절에 있다.
|
||||
- 그 절은 그 이름이 구현 파일 세 줄에만 나타나고 지목된 시험에 없다고 적는다. 구현 파일은 네 줄이고 시험에 한 줄이 있으며, 그 절이 근거로 댄 증거 파일이 다섯 줄을 전부 출력했다.
|
||||
- 페이로드를 바꿔도 그 값이 상수라는 것, 반납 없는 반복 대여에서는 단언이 깨진다는 것, 그리고 두 시험의 워크플로 지위가 다르다는 것은 이 기록에서 확인했다.
|
||||
---
|
||||
|
||||
# 부재로 판정된 결합이 원본 증거의 출력 안에 있다
|
||||
|
||||
버퍼 풀의 접근자가 자기 자바독에 유계 메모리 회귀 시험이 이 값을 단언한다고 적는다. 원본 절은 그 결합이 없다고 판정했는데, 원본이 근거로 댄 증거 파일이 그 단언 줄을 화면에 출력하고 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **소비자가 없는 fixture 셋**
|
||||
이쪽도 프로덕션 호출자가 0 이라는 것을 세어 확인했다.
|
||||
- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다**
|
||||
단언의 변별력을 묻는 규칙이고, 이 기록의 뒷부분이 그것을 자기 대상에 적용한다.
|
||||
- **커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다**
|
||||
두 사례 모두 검증이 있는 것처럼 보이지만 그 코드가 실행되지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
존재 이유는 접근자 자바독에 한 줄로 적혀 있다. 동시 대여 바이트의 정점이며 유계 메모리 회귀 시험이 이 값을 단언한다는 것이다.
|
||||
|
||||
원본 분석의 판정은 그 결합이 없다는 것이었다. 그 이름이 구현 파일 세 줄에만 나타나고 지목된 시험에는 없다는 것이 근거다.
|
||||
|
||||
## 결론
|
||||
|
||||
결합은 실재한다. 그 이름은 다섯 줄에 나오고 그중 하나가 시험의 단언이다. 그 시험의 클래스 주석이 자기를 유계 메모리 회귀라고 부르므로, 자바독이 가리킨 대상도 분명하다.
|
||||
|
||||
원본의 계수는 두 군데에서 어긋난다. 구현 파일은 세 줄이 아니라 네 줄이고, 시험 한 줄이 통째로 빠졌다.
|
||||
|
||||
빠진 이유는 검색 범위가 아니다. 원본이 근거로 댄 증거 파일의 해당 절이 구현 파일과 시험 파일을 함께 인자로 넘겨 검색했고, 다섯 줄을 전부 출력했다. 출력된 줄이 요약 단계에서 없는 것으로 적혔다.
|
||||
|
||||
시험은 실제로 돈다. 격리 태그 없이 야간 워크플로가 이름을 적어 돌린다. 풀 리퀘스트 쪽 경계 메모리 잡이 고르는 것은 두 번째 시험 하나다.
|
||||
|
||||
여기까지가 원본 판정의 정정이다. 그 뒤에 남는 것은 그 단언이 무엇을 변별하느냐다.
|
||||
|
||||
대여 계수가 실제 버퍼 용량이 아니라 고정 버퍼 크기 단위로 누적하므로, 최대 대여 바이트는 언제나 버퍼 크기의 배수다. 그리고 이 경로에서 대여는 한 번뿐이다. 오프셋이 0 이면 접두 재해시가 빌리기 전에 반환하고, 복사와 다이제스트가 한 번 빌려 종료 블록에서 반납한다.
|
||||
|
||||
실행으로 확인했다. 페이로드를 1메가, 16메가, 256메가로 바꿔도 최대 대여 바이트는 131072 로 고정된다. 이름은 최대 관측 버퍼가 페이로드를 따라 늘지 않는다고 말하는데, 실제로는 페이로드를 바꿔도 값이 그대로다.
|
||||
|
||||
그 단언이 잡는 것은 반납 없는 반복 대여다. 같은 풀에서 반납 없이 두 번 빌리면 값이 두 배가 되고 단언이 깨진다. 풀을 거치지 않는 할당은 세지 못한다.
|
||||
|
||||
그 성질을 실제로 재는 것은 두 번째 시험이다. 생성기 채널이 저장소가 채운 가장 큰 단일 버퍼를 세고, 전송 비용이 파일 크기를 따라 늘지 않는다는 것을 그 수로 보인다. 정작 그 시험은 접근자를 건드리지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : src 자바 트리 식별자 전수 검색, 원본 증거 파일 대조, 실행 탐침
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 접근자의 자바독을 읽는다.
|
||||
2. 그 이름을 src 아래 자바 파일에서 시험 트리를 포함해 검색하고 줄을 센다.
|
||||
3. 원본 절이 근거로 댄 증거 파일의 해당 절을 열어 무엇이 출력됐는지 읽는다.
|
||||
4. 그 시험에 격리 태그가 있는지, 어느 워크플로가 이름으로 지목하는지 확인한다.
|
||||
5. 대여와 반납이 어떤 단위로 누적하는지 읽는다.
|
||||
6. 같은 추가 경로를 페이로드 크기를 바꿔 가며 돌리고 최대 대여 바이트를 읽는다.
|
||||
7. 같은 풀에서 반납 없이 두 번 빌려 그 값을 읽는다.
|
||||
8. 같은 성질을 재는 두 번째 시험이 이 접근자를 부르는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
접근자의 자바독이 존재 이유를 한 줄로 적는다.
|
||||
|
||||
## 자바독이 적은 결합
|
||||
|
||||
:::evidence key="a08-f005-transferbufferpool-maxborrowedbytes" alt="버퍼 풀 접근자의 자바독과 구현. 그 이름이 src 아래 자바 파일에 나오는 곳 전수. 원본 절이 근거로 댄 증거 파일의 해당 절이 실행한 검색 명령과 그 출력. 그리고 그 시험의 애너테이션과 두 시험을 이름으로 지목하는 워크플로 줄을 출력한 터미널 기록." caption="자바독은 회귀 시험이 이 값을 단언한다고 적음 · 그 이름은 다섯 줄에 나오고 그중 하나가 시험 · 원본 증거는 구현과 시험 두 파일을 함께 검색해 다섯 줄을 전부 출력함 · 시험에 격리 태그 없음, 야간 워크플로가 이름으로 지목 — 28줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
```java
|
||||
/** Peak simultaneously-borrowed bytes; the bounded-memory regression asserts on this. */
|
||||
long maxBorrowedBytes() {
|
||||
return maxBorrowedBytes.get();
|
||||
}
|
||||
```
|
||||
|
||||
원본 분석은 그 결합이 없다고 판정했다. 그 이름이 구현 파일 세 줄에만 나타난다는 것이 근거다.
|
||||
|
||||
## 구현 네 줄과 시험 한 줄
|
||||
|
||||
```text
|
||||
test/.../LocalAppendMemoryTest.java:54: assertThat(bufferPool.maxBorrowedBytes()).isLessThanOrEqualTo(BUFFER_BYTES);
|
||||
main/.../TransferBufferPool.java:20: private final AtomicLong maxBorrowedBytes = new AtomicLong();
|
||||
main/.../TransferBufferPool.java:38: maxBorrowedBytes.accumulateAndGet(outstanding, Math::max);
|
||||
main/.../TransferBufferPool.java:56: long maxBorrowedBytes() {
|
||||
main/.../TransferBufferPool.java:57: return maxBorrowedBytes.get();
|
||||
```
|
||||
|
||||
경로 앞부분을 줄여 옮겼다. 구현 파일도 세 줄이 아니라 네 줄이고, 그 위에 시험 한 줄이 있다.
|
||||
|
||||
## 원본 증거는 그 줄을 출력했다
|
||||
|
||||
```text
|
||||
=== 8.4 doc/behaviour: bounded memory claim and its regression test ===
|
||||
$ grep -n 'maxBorrowedBytes' …/TransferBufferPool.java …/LocalAppendMemoryTest.java
|
||||
…/TransferBufferPool.java:20: private final AtomicLong maxBorrowedBytes = new AtomicLong();
|
||||
…/TransferBufferPool.java:38: maxBorrowedBytes.accumulateAndGet(outstanding, Math::max);
|
||||
…/TransferBufferPool.java:56: long maxBorrowedBytes() {
|
||||
…/TransferBufferPool.java:57: return maxBorrowedBytes.get();
|
||||
…/LocalAppendMemoryTest.java:54: assertThat(bufferPool.maxBorrowedBytes()).isLessThanOrEqualTo(BUFFER_BYTES);
|
||||
```
|
||||
|
||||
검색 명령이 구현 파일과 시험 파일을 함께 인자로 받았고, 다섯 줄을 전부 찍었다. 범위 밖이어서 못 본 것이 아니라, 출력된 줄이 요약 단계에서 없는 것으로 적혔다.
|
||||
|
||||
시험은 실제로 돈다.
|
||||
|
||||
```text
|
||||
31: @Test
|
||||
.github/workflows/fileserver-nightly.yml:103: … --tests '*LocalAppendMemoryTest'
|
||||
.github/workflows/fileserver-pr.yml:161: … --tests '*LargeFileBoundedMemoryTest'
|
||||
```
|
||||
|
||||
격리 태그가 없고 야간 워크플로가 이름으로 지목한다. 다만 풀 리퀘스트 쪽 경계 메모리 잡은 두 번째 시험만 돌린다.
|
||||
|
||||
## 그 단언이 변별하는 것
|
||||
|
||||
:::evidence key="a08-f005-transferbufferpool-maxborrowedbytes-peak" alt="자바독이 지목한 시험과 같은 추가 경로를 페이로드 1메가·16메가·256메가로 각각 돌려 최대 대여 바이트와 그것이 버퍼 크기 이하인지, 버퍼 크기의 몇 배인지 읽은 결과. 그리고 같은 풀에서 반납 없이 두 번 빌렸을 때의 값과 그 단언의 성립 여부를 출력한 터미널 기록." caption="페이로드를 256배로 늘려도 최대 대여 바이트는 131072 로 고정, 버퍼 크기의 1배 · 반납 없이 두 번 빌리면 262144 가 되어 단언이 깨짐 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
대여 계수는 실제 버퍼 용량이 아니라 고정 버퍼 크기 단위로 누적한다.
|
||||
|
||||
```java
|
||||
long outstanding = borrowedNow.addAndGet(bufferSize);
|
||||
maxBorrowedBytes.accumulateAndGet(outstanding, Math::max);
|
||||
```
|
||||
|
||||
그래서 이 값은 언제나 버퍼 크기의 배수다. 그리고 이 경로에서 대여는 한 번뿐이다 — 오프셋이 0 이면 접두 재해시가 빌리기 전에 반환하고, 복사와 다이제스트가 한 번 빌려 종료 블록에서 반납한다.
|
||||
|
||||
같은 추가 경로를 페이로드만 바꿔 돌렸다.
|
||||
|
||||
```text
|
||||
payload maxBorrowedBytes BUFFER_BYTES 이하 버퍼 크기의 배수
|
||||
1MiB 131072 예 1
|
||||
16MiB 131072 예 1
|
||||
256MiB 131072 예 1
|
||||
```
|
||||
|
||||
페이로드를 256배로 늘려도 값이 움직이지 않는다. 시험 메서드의 이름은 최대 관측 버퍼가 페이로드를 따라 늘지 않는다는 것인데, 그 값은 페이로드에 대해 상수다.
|
||||
|
||||
자명한 단언은 아니다. 반납 없이 두 번 빌리면 깨진다.
|
||||
|
||||
```text
|
||||
[대조] 반납 없이 두 번 빌리면 그 단언이 깨진다
|
||||
maxBorrowedBytes = 262144 BUFFER_BYTES 이하 = false
|
||||
```
|
||||
|
||||
잡는 것은 정점 동시 대여가 버퍼 하나를 넘는 경우다. 잡지 못하는 것은 풀을 우회한 페이로드 비례 할당이다.
|
||||
|
||||
## 그 성질을 재는 것은 두 번째 시험이다
|
||||
|
||||
```java
|
||||
* Proves that transfer cost does not scale with file size.
|
||||
*
|
||||
* <p>The property is measured, not asserted by inspection: a generator channel counts the largest
|
||||
* single buffer the store ever asked it to fill.
|
||||
```
|
||||
|
||||
생성기 채널이 실제로 채운 단일 버퍼의 최댓값을 기록한다. 이쪽은 접근자를 부르지 않는다.
|
||||
|
||||
## 정정 범위
|
||||
|
||||
자바독이 서술한 결합은 실재하고, 그 계측은 시험에 연결돼 있다. 정정할 것은 원본 판정이다. 그 단언의 변별 범위가 좁다는 것은 별개의 관찰이고, 원본 절은 그것을 다루지 않았다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
원본 절이 왜 자기 증거가 출력한 줄을 부재로 요약했는지, 그 경위는 확인하지 못했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a08-f002
|
||||
title: R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a08-f002
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a08-f002.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a08-f002
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a08-f002.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a08-f002.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/08-adapter-outbound-fileserver.md#L108 이다.
|
||||
---
|
||||
|
||||
# R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다
|
||||
|
||||
같은 리프의 두 설정 묶음이 미지의 키를 다르게 다룬다. 한쪽은 거부하고 그것을 지키는 테스트가 있다. 다른 쪽은 조용히 무시하고 그 테스트가 없다. 오타 난 상한이 적용되지 않은 채 큰 기본값이 쓰인다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **설정 오타는 실패해야 하고 무시되면 안 된다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
- **문서가 지목한 기본값 위치와 test 목록이 실제와 다르다**
|
||||
같은 리프의 다른 문서 오류다.
|
||||
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
|
||||
같은 계열의 상위 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
같은 리프 안에 설정 묶음이 둘 있다. 하나는 현재 경로이고 하나는 호환용이다.
|
||||
|
||||
두 묶음이 설정을 어떻게 다루는지 대조했다.
|
||||
|
||||
## 결론
|
||||
|
||||
다섯 축에서 다르다.
|
||||
|
||||
바인딩 타입이 다르다. 현재 경로는 불변 레코드에 미지 필드 거부를 켠다. 호환 경로는 가변 자바빈이고 기본값이라 미지 키를 무시한다.
|
||||
|
||||
루트 경로 취급이 다르다. 현재 경로는 이미 절대이고 정규화된 경로를 요구한다. 호환 경로는 상대 경로를 받아 현재 작업 디렉터리 기준으로 절대화한다.
|
||||
|
||||
기본 루트가 다르다. 현재 경로는 필수라 기본값이 없다. 호환 경로는 상대 기본값 둘을 가진다.
|
||||
|
||||
디렉터리 생성이 다르다. 현재 경로는 만들지 않고 증명을 별도로 요구한다. 호환 경로는 만든다.
|
||||
|
||||
그리고 미지 키 테스트가 현재 경로에만 있다.
|
||||
|
||||
결과의 차이가 구체적이다.
|
||||
|
||||
현재 경로에서는 설정 키의 오타가 컨텍스트를 실패시킨다. 호환 경로에서는 상한 키의 오타가 조용히 무시되고, 설정했다고 믿는 상한 대신 백만이라는 기본값이 쓰인다.
|
||||
|
||||
두 묶음이 같은 리프의 같은 성격 설정인데 한쪽만 닫힌 실패다.
|
||||
|
||||
판정은 P3 다. 호환 경로는 문서상 호환 전용이므로 우선순위를 낮춘다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
Spring Boot : 4.0.8
|
||||
확인 방식 : 두 설정 클래스와 각각의 테스트 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/142 계열에 있다.
|
||||
|
||||
1. 두 설정 묶음의 바인딩 타입을 확인한다.
|
||||
2. 미지 필드 거부 설정이 켜져 있는지 각각 확인한다.
|
||||
3. 루트 경로 정규화 방식을 대조한다.
|
||||
4. 기본값과 디렉터리 생성 여부를 대조한다.
|
||||
5. 미지 키 거부 테스트가 어느 쪽에 있는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
R2에서는 `strict-path-securty` 같은 오타가 컨텍스트를 실패시킨다. R1에서는 `app.file-export.maximum-rowz=10` 같은 오타가 조용히 무시되고, 설정했다고 믿는 상한이 적용되지 않은 채 기본값 1,000,000이 쓰인다.
|
||||
|
||||
## 같은 오타가 두 등급에서 갈리는 결과
|
||||
|
||||
:::evidence key="analysis-finding-a08-f002" alt="분석 문서 analysis/08-adapter-outbound-fileserver.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/08-adapter-outbound-fileserver.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 같은 성격의 설정인데 한쪽만 fail-closed다
|
||||
|
||||
두 selector가 같은 leaf에 있다. P3 — R1은 문서상 "compatibility only"이므로 우선순위를 낮춘다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
호환 경로의 상한 키에 오타를 넣고 실제로 백만이 쓰이는지 실행하지 않았다. 바인딩 설정상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a08-f003
|
||||
title: 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a08-f003
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: analysis-finding-a08-f003
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a08-f003.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a08-f003.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/08-adapter-outbound-fileserver.md#L122 이다.
|
||||
---
|
||||
|
||||
# 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다
|
||||
|
||||
README 가 선택 스위치의 기본값이 부트스트랩 설정 파일에 꺼짐으로 들어 있다고 적는다. 그 파일에 해당 키가 없다. 그리고 테스트 목록의 첫 항목은 이 리프가 아니라 애플리케이션 코어에 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **두 selector의 설정 취급이 비대칭이고 검증된 쪽은 하나뿐이다**
|
||||
같은 리프의 설정 사례다.
|
||||
- **README의 세 가지 사실 오류**
|
||||
같은 계열의 문서 오류다.
|
||||
- **동작이 옳아도 문서가 가리킨 자리는 틀릴 수 있다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
README 가 두 가지를 적는다. 선택 스위치의 기본값 위치와 이 리프를 검증하는 테스트 목록이다.
|
||||
|
||||
## 결론
|
||||
|
||||
첫째가 어긋난다.
|
||||
|
||||
README 는 선택 스위치가 부트스트랩 설정 파일에서 꺼짐으로 기본값을 갖는다고 적는다.
|
||||
|
||||
그 파일에는 현재 경로의 활성화 키도 호환 경로의 활성화 키도 없다. 비슷한 접두사로 걸리는 두 줄은 주석이다.
|
||||
|
||||
실효 기본값은 다른 경로로 만들어진다. 속성이 없으면 조건부 애너테이션이 맞지 않고 빈이 만들어지지 않는다.
|
||||
|
||||
그러므로 동작 자체는 옳다. 꺼진 것이 기본이다.
|
||||
|
||||
틀린 것은 그 결과가 어디서 오는지에 대한 서술이다. 문서가 가리킨 자리에 그 키가 없다.
|
||||
|
||||
둘째도 어긋난다.
|
||||
|
||||
README 의 테스트 목록 첫 항목이 이 리프에 없다. 애플리케이션 코어에 있는 클래스다.
|
||||
|
||||
판정은 P3 다. 둘 다 동작이 아니라 서술의 문제다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
Spring Boot : 4.0.8
|
||||
확인 방식 : 설정 파일 키 검색과 테스트 클래스 위치 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/142 계열에 있다.
|
||||
|
||||
1. README 의 기본값 서술 줄을 읽는다.
|
||||
2. 지목된 설정 파일에서 두 활성화 키를 검색한다.
|
||||
3. 걸리는 줄이 주석인지 확인한다.
|
||||
4. 조건부 애너테이션이 속성 부재에서 어떻게 동작하는지 확인한다.
|
||||
5. 테스트 목록의 첫 항목을 저장소 전체에서 찾는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
README는 R2 selector가 "`app-bootstrap/application.yml`에서 `false`로 기본값을 갖는다"고 적는다. 그 파일에 `app.fileserver.enabled`도 `app.file-export.enabled`도 없다(`142-...` §8.4e; `app.fileserver`로 걸리는 두 줄은 주석이다).
|
||||
|
||||
## FilePublicationContractTest 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a08-f003" alt="코드베이스에서 FilePublicationContractTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FilePublicationContractTest 코드베이스 검색 — 1줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 동작은 옳고 문서가 가리킨 자리가 비어 있다
|
||||
|
||||
실효 기본값은 "속성 부재 → `@ConditionalOnProperty` 미매치 → bean 없음"이다.
|
||||
|
||||
## test 목록의 첫 항목도 다른 곳에 있다
|
||||
|
||||
README의 Tests 목록 첫 항목 `FilePublicationContractTest`는 이 leaf가 아니라 `application-core`에 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
그 키가 과거에 설정 파일에 있었는지 확인하지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a08-f004
|
||||
title: 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "근사에 불과하다"고 적은 사전검사다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a08-f004
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a08-f004.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a08-f004
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a08-f004.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a08-f004.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/08-adapter-outbound-fileserver.md#L376 이다.
|
||||
---
|
||||
|
||||
# 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "근사에 불과하다"고 적은 사전검사다
|
||||
|
||||
이 모듈은 디스크립터를 따라 내려가는 방식으로 심볼릭 링크 공격을 구조적으로 막는다. 콘텐츠를 보이게 만드는 마지막 단계만 경로 기반으로 남아 있고, 그 단계를 지키는 사전검사는 모듈 자신이 근사에 불과하다고 적어 둔 메서드다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검사를 더 하는 것은 창을 좁힐 뿐 닫지 않는다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
- **두 selector의 설정 취급이 비대칭이고 검증된 쪽은 하나뿐이다**
|
||||
같은 리프의 다른 사례다.
|
||||
- **계측 필드가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다**
|
||||
같은 리프의 문서 결합 사례다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 모듈의 지역 플랫폼에 남은 파일 시스템 정적 호출을 전수 조사했다.
|
||||
|
||||
대부분은 정당하다. 루트 열기는 문서가 피할 수 없는 하나의 경로 해석이라고 적은 것이고, 시작 시 능력 탐침은 격리된 영역에서 돌며, 고아 검사와 상태 조회와 사용량 탐침은 다른 하위 범위다.
|
||||
|
||||
## 결론
|
||||
|
||||
문제는 쓰기 경로에 남은 다섯 호출이다.
|
||||
|
||||
원자적 이동 발행자의 발행 이동과 그 실패 분류를 위한 존재 검사 둘, 발행 검증의 크기 조회, 메타데이터 포인터 발행자의 준비 파일 삭제다.
|
||||
|
||||
그중 발행 이동이 핵심이다.
|
||||
|
||||
발행자는 그 이동 직전에 루트와 대상 부모 사이에 심볼릭 링크가 없는지 검사한다. 경로 기반 사전검사다.
|
||||
|
||||
그런데 그 메서드의 자바독이 스스로를 이렇게 설명한다.
|
||||
|
||||
능력 탐침을 위해 남겨 두었으며 탐침은 여전히 경로명으로 추론한다. production 접근은 더 이상 여기에 의존하지 않는다. 디스크립터를 하나씩 따라 내려가며 링크를 따르지 않는 방식이 심볼릭 링크 성분을 구조적으로 거부하고, 사전검사는 그것을 근사할 수 있을 뿐이라는 것이다.
|
||||
|
||||
즉 production 이 더 이상 의존하지 않는다고 적힌 메서드를, 콘텐츠를 보이게 만드는 바로 그 단계가 유일한 보호로 쓴다.
|
||||
|
||||
같은 모듈의 다른 문서가 겨냥한 패턴 그대로다. 검사를 더 하는 것은 창을 좁힐 뿐 닫지 않는다는 문장이다.
|
||||
|
||||
같은 불일치가 파일 길이 조회에서도 보인다.
|
||||
|
||||
판정은 P3 다.
|
||||
|
||||
실제 악용에는 저장소 루트 안쪽 쓰기 권한이 필요하다. 이 리프가 그 루트의 소유자와 권한을 증명하는 것은 현재 경로뿐이고, 플랫폼 저장소 루트의 증명은 부트스트랩의 시작 검증기 몫이다.
|
||||
|
||||
그래서 도달성은 배포 형상에 달려 있다.
|
||||
|
||||
그럼에도 기록하는 이유는 모듈 자신의 문서가 이 패턴을 명시적으로 불충분하다고 선언했다는 점이다.
|
||||
|
||||
수정은 발행 이동을 디스크립터 기반 도우미로 옮겨 부모 서술자 상대 이동을 쓰고, 크기 조회를 채널의 속성 읽기로 바꾸는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 정적 호출 전수 조사와 자바독 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/145 계열에 있다.
|
||||
|
||||
1. 지역 플랫폼의 파일 시스템 정적 호출을 전수 조사한다.
|
||||
2. 각 호출이 어느 경로에 속하는지 분류한다.
|
||||
3. 쓰기 경로에 남은 호출을 추린다.
|
||||
4. 발행 이동 직전의 사전검사가 무엇인지 확인한다.
|
||||
5. 그 사전검사 메서드의 자바독을 읽는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "a precheck could only ever approximate"라고 적은 사전검사다.
|
||||
|
||||
## LocalPersistentRootAttestor 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a08-f004" alt="코드베이스에서 LocalPersistentRootAttestor 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalPersistentRootAttestor 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 도달성이 배포 형상에 달려 있다
|
||||
|
||||
실제 악용에는 스토리지 루트 안쪽 쓰기 권한이 필요하고, 이 leaf가 그 루트의 소유자·권한을 증명하는 것은 R2 경로(`LocalPersistentRootAttestor`)뿐이며, 플랫폼 저장소 루트의 증명은 `app-bootstrap`의 startup validator 몫이다. 심각도를 P3로 두는 이유가 그것이다.
|
||||
|
||||
## 그럼에도 기록하는 이유
|
||||
|
||||
모듈 자신의 문서가 이 패턴을 명시적으로 불충분하다고 선언했다.
|
||||
|
||||
## 수정
|
||||
|
||||
발행 rename을 `SecureDirectoryWalk.inParentOf`로 옮겨 부모 서술자 상대 `move`를 쓰고, `sizeOf`를 `channels.readAttributes`로 바꾸는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
심볼릭 링크를 실제로 심어 발행 단계를 통과시키는 재현을 하지 않았다. 그것은 저장소 루트 안쪽 쓰기 권한을 전제한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a09-f001
|
||||
title: production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a09-f001
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a09-f001.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a09-f001
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a09-f001.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a09-f001.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L106 이다.
|
||||
---
|
||||
|
||||
# production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다
|
||||
|
||||
개발용 제공자를 운영에서 쓰지 말라는 금지가 문서에 있고 그것을 강제하는 코드는 한 곳이다. 그 코드는 거부 검사인데 판정 근거가 이름 두 개의 허용 목록이다. 다른 이름을 쓰는 분기는 검사를 통과한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **거부 검사의 근거를 허용 목록으로 두면 목록 밖이 통과한다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
- **사라진 가드가 문서에만 남았고 그 부재는 이미 승인된 결정이다**
|
||||
같은 계열의 강제 수단 사례다.
|
||||
- **발행 rename만 경로 기반이고 그것을 지키는 것은 모듈 자신이 근사에 불과하다고 적은 사전검사다**
|
||||
도달성이 형상에 달린 다른 사례다.
|
||||
|
||||
## 문제
|
||||
|
||||
리프 지침 문서의 금지 목록에 개발용 제공자를 운영에서 쓰는 것이 있다.
|
||||
|
||||
그것을 강제하는 코드가 어디 있는지 찾았다.
|
||||
|
||||
## 결론
|
||||
|
||||
한 곳이다.
|
||||
|
||||
활성 프로파일 목록을 소문자로 만들고, 그중 하나가 짧은 운영 이름이거나 긴 운영 이름과 같은지 본다.
|
||||
|
||||
이 저장소가 짧은 이름의 프로파일 설정 파일을 싣고 있으므로 현재 형상에서는 맞는다.
|
||||
|
||||
그리고 같은 방식으로 운영을 판정하는 리프는 이것 하나뿐이다. 저장소 전체가 공유하는 운영 판별 장치가 없다.
|
||||
|
||||
문제는 방향이다.
|
||||
|
||||
이것은 거부 검사인데 판정 근거가 허용 목록 두 개다.
|
||||
|
||||
지역이나 환경을 이름에 붙이거나 축약형이나 다른 낱말을 쓰는 분기는 이 검사를 통과한다. 그리고 개발용 제공자가 운영에서 조용히 선택된다.
|
||||
|
||||
그 제공자는 README 가 현재 경로 전용 개발 제공자라고 적은 것이다.
|
||||
|
||||
실패는 시작 시점이 아니라 데이터가 로컬 디스크에 쌓인 뒤에 드러난다.
|
||||
|
||||
판정은 P3 다. 이 저장소 형상에서는 도달하지 않는다.
|
||||
|
||||
기록하는 이유는 셋이다. 지침 문서가 금지 항목으로 명시했고, 강제 수단이 문자열 둘이며, 분기가 프로파일 이름을 바꾸는 것은 평범한 일이다.
|
||||
|
||||
수정은 운영 판별을 뒤집는 것이다. 이름을 묻는 대신 개발용 제공자를 허용한다는 명시적 설정을 요구하는 형태다. 이름이 아니라 의도를 묻는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
Spring Boot : 4.0.8
|
||||
확인 방식 : 강제 코드 전수 검색과 저장소 전체 운영 판별 장치 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/150 계열에 있다.
|
||||
|
||||
1. 지침 문서의 금지 목록을 읽는다.
|
||||
2. 그 금지를 강제하는 코드를 저장소에서 찾는다.
|
||||
3. 판정 조건이 무엇과 비교하는지 확인한다.
|
||||
4. 같은 방식으로 운영을 판정하는 다른 리프가 있는지 센다.
|
||||
5. 개발용 제공자의 README 서술을 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
CLAUDE.md의 Forbidden 목록에 "local-dev in production"이 있고, 그것을 강제하는 코드는 이것 하나다.
|
||||
|
||||
```java
|
||||
private boolean productionProfileActive() {
|
||||
return activeProfiles.stream()
|
||||
.map(profile -> profile.toLowerCase(Locale.ROOT))
|
||||
.anyMatch(profile -> profile.equals("prod") || profile.equals("production"));
|
||||
}
|
||||
```
|
||||
|
||||
## 금지를 강제하는 코드 한 곳
|
||||
|
||||
:::evidence key="analysis-finding-a09-f001" alt="분석 문서 analysis/09-adapter-outbound-objectstorage.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/09-adapter-outbound-objectstorage.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 현재 형상에서는 맞는다
|
||||
|
||||
이 저장소가 `application-prod.yml`을 싣고 있다. 그리고 `150-...` §8.2c에서 확인했듯 같은 방식으로 production을 판정하는 leaf는 이것 하나뿐이다 — 저장소 전체가 공유하는 production 판별 장치가 없다.
|
||||
|
||||
## 문제는 방향이다
|
||||
|
||||
이것은 **거부** 검사인데 판정 근거가 **허용 목록 두 개**다. `prd`, `production-eu`, `live`, `prod-apac` 같은 이름을 쓰는 fork는 이 검사를 통과하고, `filesystem-local-dev`가 production에서 조용히 선택된다 — 그 provider는 README가 "R1-only development provider"라고 적은 것이다. 실패는 startup이 아니라 데이터가 로컬 디스크에 쌓인 뒤에 드러난다.
|
||||
|
||||
## 기록하는 세 이유
|
||||
|
||||
(a) CLAUDE.md가 금지 항목으로 명시했고 (b) 강제 수단이 두 문자열이며 (c) fork가 프로파일 이름을 바꾸는 것은 평범한 일이다. 수정은 production 판별을 명시적 설정(예: `app.object-storage.allow-local-dev=true`를 요구)으로 뒤집는 것 — 이름이 아니라 의도를 묻는 형태다. P3.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
목록 밖 이름의 프로파일로 실제로 띄워 개발용 제공자가 선택되는지 재현하지 않았다. 조건식상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a09-f004
|
||||
title: 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a09-f004
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a09-f004.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a09-f004
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a09-f004.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a09-f004.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L485 이다.
|
||||
---
|
||||
|
||||
# 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다
|
||||
|
||||
직접 전송 능력이 아직 배선되지 않았다고 README 가 선언한다. 그런데 설정은 그 능력을 계속 받아들이고 런타임은 자격증명을 쥔 서명자를 실제로 만든다. 만들어진 제공자를 꺼내 가는 코드는 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **두 개의 outbox 중 하나만 조립되어 있다**
|
||||
같은 형태의 미조립 사례다.
|
||||
- **production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다**
|
||||
같은 리프의 다른 사례이자 반대 사례다.
|
||||
- **문서가 선언한 경계는 코드가 닫아야 경계다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
README 가 직접 업로드와 직접 다중 파트가 아직 배선되지 않았다고 적는다.
|
||||
|
||||
그 선언이 코드로도 강제되는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
강제되지 않는다. 네 단계로 확인된다.
|
||||
|
||||
첫째, 두 능력은 목적지 요구사항으로 선언할 수 있고 바인딩 컴파일러가 그것을 제공자 능력으로 번역한다.
|
||||
|
||||
둘째, 제공자 바인딩의 프로파일 컴파일은 자체 호스팅 구현에 대해서만 이 두 주장을 거부한다. 정확한 판본이 조건부 관리 변형을 기본 지원한다고 주장할 수 없다는 이유다. 클라우드 프로파일에는 대응하는 거부가 없다.
|
||||
|
||||
셋째, 그러면 제공자 기여자가 두 스위치 중 하나라도 켜져 있을 때 서명자를 할당하고 직접 전송 제공자와 직접 다중 파트 제공자를 만들어 선택 팩토리에 넣는다.
|
||||
|
||||
넷째, 그 둘을 팩토리에서 꺼내 가는 코드가 없다. 전수 검색 결과가 0 이다.
|
||||
|
||||
즉 클라우드 바인딩에서 직접 업로드를 요구하는 배포는 시작을 통과하고 서명자를 할당하고 두 제공자를 조립하고, 직접 전송 포트는 여전히 하나도 얻지 못한다.
|
||||
|
||||
README 가 말한 미배선은 사실이다. 그 사실을 강제하는 것이 문서뿐이다.
|
||||
|
||||
이 리프 안에 정반대의 사례가 있어서 대비가 분명하다.
|
||||
|
||||
개발용 제공자는 운영 프로파일에서 컴파일 시점에 거부된다. 자체 호스팅 구현의 능력 과대 주장도 컴파일 시점에 거부된다.
|
||||
|
||||
같은 파일이 같은 종류의 자격 없음 판정을 클라우드와 직접 전송 조합에 대해서만 하지 않는다.
|
||||
|
||||
판정은 P2 다.
|
||||
|
||||
데이터 위험은 없다. 없는 포트는 호출될 수 없다.
|
||||
|
||||
위험은 둘이다. 운영자가 켰다고 믿는 기능이 없다는 것과, 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것이다.
|
||||
|
||||
수정은 셋 중 하나다. 조정자를 조건부로 조립하거나, 미배선 동안 두 능력 요구를 제공자 종류와 무관하게 컴파일 단계에서 거부하거나, 능력이 켜져도 서명자를 만들지 않도록 조립을 뒤로 미루는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 바인딩 컴파일러와 제공자 기여자 코드 확인, 소비자 전수 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/154 계열에 있다.
|
||||
|
||||
1. README 의 미배선 선언을 읽는다.
|
||||
2. 두 능력이 목적지 요구사항으로 선언 가능한지 확인한다.
|
||||
3. 프로파일 컴파일에서 어느 제공자에 대해 거부가 있는지 확인한다.
|
||||
4. 제공자 기여자가 어떤 조건에서 서명자를 만드는지 확인한다.
|
||||
5. 만들어진 두 제공자를 팩토리에서 꺼내는 코드를 검색한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다.
|
||||
|
||||
## 문서에만 있는 R0 경계
|
||||
|
||||
:::evidence key="analysis-finding-a09-f004" alt="분석 문서 analysis/09-adapter-outbound-objectstorage.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/09-adapter-outbound-objectstorage.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 데이터 위험은 없다
|
||||
|
||||
없는 port는 호출될 수 없다. P2.
|
||||
|
||||
## 위험 둘
|
||||
|
||||
(a) 운영자가 켰다고 믿는 기능이 없다는 것, (b) 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것.
|
||||
|
||||
## 수정 방향 셋
|
||||
|
||||
coordinator를 조건부로 조립하거나, R0인 동안 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구를 compile 단계에서 provider 종류와 무관하게 거부하거나, capability가 켜져도 presigner를 만들지 않도록 조립을 뒤로 미루는 것.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
클라우드 바인딩으로 실제로 띄워 서명자가 살아 있는 것을 확인하지 않았다. 조립 조건상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a09-f006
|
||||
title: nonce replay 경계가 결과를 읽고 버린다
|
||||
topic: file-transfer-and-storage
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a09-f006
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a09-f006.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a09-f006
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a09-f006.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a09-f006.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L589 이다.
|
||||
---
|
||||
|
||||
# nonce replay 경계가 결과를 읽고 버린다
|
||||
|
||||
인터페이스가 스스로를 내구적 비교 후 교체 재생 경계라고 부른다. 호출부는 청구 결과를 받아 변수에 담고, 어느 값이든 발행을 그대로 진행한다. 결과가 바꾸는 것은 종결 기록을 쓸지 여부뿐이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검사 결과를 읽고 버리면 검사가 아니다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
- **미배선 경계가 문서에만 있고 compile 경로에서 닫히지 않는다**
|
||||
같은 리프의 다른 경계 사례다.
|
||||
- **멱등은 결과를 같게 만들지만 경계를 대신하지는 않는다**
|
||||
피해가 제한된 이유이자 그것이 변명이 되지 않는 이유다.
|
||||
|
||||
## 문제
|
||||
|
||||
승인 문서의 논스가 한 번만 쓰인다는 성질을 지키는 저장소가 있다.
|
||||
|
||||
그 저장소의 청구 메서드를 호출부가 어떻게 쓰는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
청구 결과를 변수에 담고, 다음 줄에서 발행을 부른다.
|
||||
|
||||
그다음에야 결과를 본다. 종결 재생이 아니면 종결 기록을 남긴다.
|
||||
|
||||
결과 값은 셋이다. 청구됨과 정확한 재생과 종결 재생이다.
|
||||
|
||||
어느 값이든 발행은 그대로 진행된다.
|
||||
|
||||
즉 청구 결과가 바꾸는 것은 종결 기록을 쓸지 여부뿐이다. 인터페이스 자바독은 자신을 내구적 비교 후 교체 재생 경계라고 부르는데, 경계로서 무엇도 막지 않는다.
|
||||
|
||||
실제 피해는 제한적이다.
|
||||
|
||||
발행이 연산 키 기반 멱등이다. 이미 소진된 논스로 다시 들어와도 결과는 재생됨이고 두 번째 객체가 생기지 않는다.
|
||||
|
||||
그래서 판정은 P3 다.
|
||||
|
||||
그럼에도 기록하는 이유는 셋이다.
|
||||
|
||||
이름이 약속하는 것과 다르다. 종결 기록에 넘기는 기대 개정 번호가 항상 0 이라 비교 후 교체의 인자로서도 고정값이다. 그리고 승인 문서의 논스가 한 번만 쓰인다는 성질이 이 코드로는 보장되지 않는다.
|
||||
|
||||
수정은 종결 재생에서 발행 전에 거부하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 호출부와 인터페이스 자바독 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/154 계열에 있다.
|
||||
|
||||
1. 재생 저장소 인터페이스의 자바독을 읽는다.
|
||||
2. 청구 결과 열거값 셋을 확인한다.
|
||||
3. 호출부에서 결과가 어떻게 쓰이는지 확인한다.
|
||||
4. 발행 호출이 결과보다 앞인지 뒤인지 본다.
|
||||
5. 종결 기록에 넘기는 기대 개정 번호가 무엇인지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ClaimResult`는 `CLAIMED` / `EXACT_REPLAY` / `TERMINAL_REPLAY` 셋인데, 어느 값이든 발행은 그대로 진행된다. claim 결과가 바꾸는 것은 terminal 기록을 쓸지 여부뿐이다.
|
||||
|
||||
## ClaimResult 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a09-f006" alt="코드베이스에서 ClaimResult 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimResult 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 경계라고 부르지만 아무것도 막지 않는다
|
||||
|
||||
인터페이스 javadoc은 자신을 "Durable compare-and-set nonce replay boundary"라고 부른다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
소진된 논스로 두 번 들어와 결과가 재생됨이 되는지 실행하지 않았다. 멱등 키 구현상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user