Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f007-dns.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

14 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE a11-f007-dns 혼동이라 적어 둔 이름이 아직 검사 문구에 있다 http-client-and-resilience clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a11-f007-dns 2026-09-02 case-a11-f007-dns.body.md
key file
a11-f007-dns ../../../final/evidence/rendered/a11-f007-dns.svg
key file
a11-f007-dns-overloads ../../../final/evidence/rendered/a11-f007-dns-overloads.svg
../../../final/evidence/raw/a11-f007-dns.txt
../../../final/evidence/raw/a11-f007-dns-overloads.txt
원본 분석 절은 final/document.md#a11#L583 이다. 등급은 P3 이다. 블로킹 오버로드에만 그 검사와 주석이 있다는 대조, 반응형 오버로드는 동적 대상 안정만 본다는 지적, 그래서 주석이 혼동이라 적은 상태가 반응형 경로에 남아 있다는 판정, 출하된 두 반응형 전송의 두 플래그가 같은 값이라 지금 노출이 없다는 확인, 프로파일 검증기가 동적 모드와 실험적 전송 조합을 이미 거부한다는 사실, 위험이 열리는 조합과 수정 방향이 그 절에 있다.
이 기록이 더한 것은 넷이다. 36 의 문구가 자기가 읽지 않는 플래그의 이름을 쓴다는 것 — 혼동은 과거형이 아니다. 그래서 이 두 검사에 걸린 유일한 test 가 36 의 문구만으로 통과하므로 43 을 지워도 빨개지지 않는다는 것. 두 플래그 조합 넷을 실제로 넣어, 갈리는 것이 한 조합뿐이고 나머지 거부에서 반응형이 내놓는 문구가 검사하지 않은 능력의 이름이라는 것. 그리고 오늘 막고 있는 것이 프로파일 검증기가 아니라 반응형 제공자 맵에 전송이 하나뿐이라는 사실이고, 그 맵은 빈 부재 조건이라 배포가 갈아 끼울 수 있다는 것이다.

혼동이라 적어 둔 이름이 아직 검사 문구에 있다

전송 능력 검증기의 블로킹 오버로드에 동적 모드 검사가 둘 있다. 앞엣것은 안정 플래그를 읽으면서 뒤엣것이 지키는 능력의 이름을 문구에 쓴다. 반응형 오버로드에는 앞엣것만 있고, 그래서 그 문구가 반응형 거부에도 그대로 나온다.

관계

  • 두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다 두 사례 모두 오버로드가 둘인데 검사가 한쪽에만 있다.
  • 재생 가능으로 인증된 본문이 다른 바이트를 낸다 같은 리프의 다른 검사 사례다.
  • 가드의 존재가 곧 테넌트 격리 보장은 아니다 지금 노출이 없는 것과 검사가 있는 것은 다르다.

문제

능력 검증기는 블로킹용과 반응형용 오버로드를 따로 갖는다.

두 오버로드가 같은 검사를 하는지 대조했다.

결론

블로킹 쪽에만 있는 검사가 하나 있다.

동적 모드인데 호출 범위 검증된 DNS 고정 능력이 없으면 누락 목록에 넣는 검사다. 그 앞에는 같은 조건에서 동적 대상 안정 능력을 보는 검사가 따로 있다.

뒤엣것의 주석이 이유를 적는다. 그 능력 플래그가 모든 능력 레코드에 선언되어 있었지만 아무도 읽지 않았다는 것이다. 서버 측 요청 위조 방어의 주소 검증이 소켓까지 살아남는지를 결정하는 것이 그 능력이므로, 그것이 없는 전송은 동적 대상 안정 플래그가 무엇을 말하든 동적 대상을 서비스할 수 없다는 것이다. 둘이 혼동되고 있었다는 것이다.

그런데 앞엣것의 문구가 자기가 읽지 않는 플래그의 이름을 쓴다. 그 검사는 안정 플래그를 보면서 검증된 DNS 고정이 없다고 적고, 고정 플래그를 실제로 읽는 것은 뒤엣것이다. 주석이 지나간 일로 적은 혼동이 이 문구에는 아직 남아 있다.

그래서 하나뿐인 그 test 가 둘째 줄을 놓친다. 그 test 는 두 플래그가 모두 거짓인 능력을 넣고 메시지에 검증된 DNS 고정이 들어 있는지만 본다. 앞엣것의 문구만으로 맞는 조건이다. 뒤엣것을 통째로 지워도 초록이다.

반응형 오버로드에는 앞엣것만 있다.

실행으로 확인했다. 동적 프로파일 하나에 두 플래그 조합 넷을 넣었다. 둘 다 참이면 양쪽 통과다. 안정이 거짓인 두 조합은 양쪽 다 거부하는데, 그때 반응형이 내놓는 문구가 검증된 DNS 고정이 없다는 말이다. 고정이 거짓이고 안정이 참인 조합에서만 갈린다. 블로킹은 호출 범위 항목으로 거부하고 반응형은 통과한다.

지금 그 조합은 만들어지지 않는다. 다섯 능력 모두 두 플래그에 같은 값을 넣어 둔다. 셋은 둘 다 참이고 둘은 둘 다 거짓이다.

막고 있는 것은 프로파일 검증기가 아니다. 프로파일 검증기는 동적 모드와 두 전송 상수의 조합을 이름으로 막는데, 전송이 하나 늘면 그 목록에 없다. 오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 기본 자동 구성이 반응형 제공자 맵에 전송 하나만 넣기 때문이다.

그 맵은 빈 부재 조건이 붙어 있다. 자기 맵을 올리는 배포는 소스를 건드리지 않고 그 자리의 능력을 갈아 끼울 수 있다.

판정은 P3 다.

위험은 그렇게 올린 능력이 동적 대상 안정을 참으로, 호출 범위 검증된 DNS 고정을 거짓으로 선언하는 경우다. 주석이 못박은 정확히 그 조합이 반응형 쪽에서는 통과한다.

수정은 38~44 를 반응형 오버로드에 복사하는 것이다. 다만 출하된 다섯이 두 플래그를 같은 값으로 두고 있으므로 오늘 바뀌는 판정은 없다.

반응형 레코드에는 어느 검사도 읽지 않는 플래그가 둘 더 있다. 주석이 적은 상태가 그 둘에는 그대로다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 두 오버로드 대조, 플래그 참조 전수 확인, 실행 탐침 소스 수정 : x

재현 조건

  1. 전송 능력 검증기의 두 오버로드를 나란히 읽는다.
  2. 블로킹 쪽 마지막 두 검사가 각각 어떤 플래그를 읽고 어떤 문구를 넣는지 대조한다.
  3. 그 두 검사에 걸린 test 를 찾아 무엇을 단언하는지 읽는다.
  4. 세 능력 플래그를 읽는 곳을 저장소 전체에서 센다.
  5. 출하된 능력 다섯이 두 플래그에 넣는 값을 뽑는다.
  6. 반응형 제공자 맵에 어떤 전송이 들어가는지, 그 빈에 어떤 조건이 붙어 있는지 읽는다.
  7. 프로파일 검증기가 동적 모드에 대해 이름으로 막는 전송을 확인한다.
  8. 동적 프로파일 하나에 두 플래그 조합 넷을 넣어 양쪽 오버로드에 통과시킨다.

본문

전송 능력 검증기에 오버로드가 둘 있다.

한쪽에만 있는 검사

:::evidence key="a11-f007-dns" alt="전송 능력 검증기의 블로킹 오버로드에 있는 마지막 두 검사와 그 사이의 주석, 반응형 오버로드의 마지막 검사, 그 두 검사에 걸린 유일한 test 가 단언하는 것, 세 능력 플래그를 읽는 곳 전부, 출하된 능력 다섯이 두 플래그에 넣는 값, 반응형 제공자 맵에 들어가는 전송과 그 빈에 붙은 조건, 그리고 프로파일 검증기가 동적 모드에 대해 이름으로 막는 전송을 출력한 터미널 기록." caption="36 은 안정 플래그를 읽으면서 검증된 DNS 고정이라는 문구를 넣고, 고정 플래그를 읽는 것은 43 이다 · test 는 검증된 DNS 고정이라는 문자열만 확인한다 · 출하된 능력 다섯이 두 플래그를 같은 값으로 선언한다 · 반응형 제공자 맵에는 REACTOR_NETTY 하나가 들어가고 그 빈은 빈 부재 조건이다 — 68줄 · exit 0" zoom="true" :::

블로킹 쪽의 마지막 두 검사다.

35:    if (profile.mode() == ClientMode.DYNAMIC && !capabilities.dynamicTargetStable()) {
36:      missing.add("validated DNS pinning for dynamic targets");
37:    }
38:    if (profile.mode() == ClientMode.DYNAMIC && !capabilities.validatedDnsPinning()) {
39:      // `validatedDnsPinning` was declared on every capability record and read by nothing. It is
40:      // the capability that decides whether the SSRF address validation survives to the socket, so
41:      // a transport that does not have it cannot serve a dynamic target no matter what its
42:      // `dynamicTargetStable` flag says — the two were being conflated.
43:      missing.add("call-scoped validated DNS pinning");
44:    }

반응형 쪽은 여기서 끝난다.

64:    if (profile.mode() == ClientMode.DYNAMIC && !capabilities.dynamicTargetStable()) {
65:      missing.add("validated DNS pinning for dynamic targets");
66:    }
67:    reject(profile, missing);
68:  }

36 의 문구가 가리키는 플래그

36 은 자기가 읽는 플래그를 가리키지 않는다. 그 검사가 보는 것은 dynamicTargetStable 인데 적히는 말은 검증된 DNS 고정이고, validatedDnsPinning 을 실제로 읽는 검사는 43 이다. 주석이 과거형으로 적은 혼동이 이 문구에 남아 있다.

그래서 이 두 검사에 걸린 유일한 test 가 43 을 붙들지 못한다.

39:  void rejectsDynamicModeWithoutValidatedPinning() {
40-    ClientProfile dynamic = ClientProfiles.builder("webhook").mode(ClientMode.DYNAMIC).build();
41-    assertThatThrownBy(
42-            () ->
43-                validator.validate(
44-                    dynamic, BlockingTransportCapabilities.lightweightHttp11AndHttp2()))
45-        .isInstanceOf(HttpConfigurationException.class)
46-        .hasMessageContaining("validated DNS pinning");

넣는 능력은 두 플래그가 모두 거짓이고, 단언은 메시지에 그 문자열이 있는지다. 36 의 문구만으로 맞는다. 38~44 를 통째로 지워도 이 test 는 초록이다.

넣어 보면

:::evidence key="a11-f007-dns-overloads" alt="동적 대상 프로파일 하나에 두 플래그 조합 넷을 넣어, 블로킹 오버로드와 반응형 오버로드가 각각 통과시키는지 거부하는지와 거부할 때 적히는 누락 항목을 출력한 터미널 기록. 출하된 두 반응형 전송의 두 플래그 값도 함께 적는다. 픽스처는 고정 리비전 소스에서 직접 컴파일한다." caption="안정이 거짓인 두 조합은 양쪽 다 거부하고 반응형 문구는 검증된 DNS 고정이 없다는 말이다 · 고정이 거짓이고 안정이 참인 조합에서만 블로킹이 거부하고 반응형이 통과한다 — 20줄 · exit 0" zoom="true" :::

pinning=true  stable=true 
    블로킹 : 통과
    반응형 : 통과
pinning=false stable=false
    블로킹 : 거부 — validated DNS pinning for dynamic targets, call-scoped validated DNS pinning
    반응형 : 거부 — validated DNS pinning for dynamic targets
pinning=true  stable=false
    블로킹 : 거부 — validated DNS pinning for dynamic targets
    반응형 : 거부 — validated DNS pinning for dynamic targets
pinning=false stable=true 
    블로킹 : 거부 — call-scoped validated DNS pinning
    반응형 : 통과

안정이 거짓인 두 조합은 양쪽 다 거부한다. 그때 반응형이 운영자에게 내놓는 말이 검증된 DNS 고정이 없다는 문구다. 검사한 것은 다른 플래그다.

갈리는 것은 마지막 줄뿐이다.

지금 그 조합은 만들어지지 않는다

    apacheClassic                validatedDnsPinning=true   dynamicTargetStable=true
    http11AndHttp2               validatedDnsPinning=true   dynamicTargetStable=true
    lightweightHttp11AndHttp2    validatedDnsPinning=false  dynamicTargetStable=false
    reactorNetty                 validatedDnsPinning=true   dynamicTargetStable=true
    jettyHttp3Experimental       validatedDnsPinning=false  dynamicTargetStable=false

출하된 능력 다섯이 전부 두 플래그를 같은 값으로 선언한다. 38~44 를 복사해도 오늘 바뀌는 판정은 없다.

막고 있는 것은 프로파일 검증기가 아니다

173-    if (profile.mode() == ClientMode.DYNAMIC
174-        && (profile.transport() == TransportType.JDK
175-            || profile.transport() == TransportType.JETTY)) {
176:      out.add(violation("DYNAMIC_TARGET_TRANSPORT_UNSUPPORTED", profile, "transport"));

이 검사가 이름으로 막는 것은 두 상수다. 전송이 하나 늘면 여기에는 걸리지 않는다.

오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 다른 데 있다.

  @Bean
  @ConditionalOnMissingBean(name = "httpClientReactiveTransportProviders")
  Map<TransportType, ReactiveTransportProvider> httpClientReactiveTransportProviders(
      ObjectProvider<MeterRegistry> meterRegistry, TlsMaterialProvider tlsMaterialProvider) {
    Map<TransportType, ReactiveTransportProvider> providers = new EnumMap<>(TransportType.class);
    providers.put(
        TransportType.REACTOR_NETTY,

맵에 들어가는 전송이 하나다. 그리고 그 빈에는 빈 부재 조건이 붙어 있다. 배포가 자기 맵을 올리면 소스를 갈라내지 않고 다른 능력을 그 자리에 넣을 수 있다.

아직 아무도 읽지 않는 플래그

    TransportCapabilityValidator.java:38:    if (profile.mode() == ClientMode.DYNAMIC && !capabilities.validatedDnsPinning()) {
    ReactiveTransportCapabilities.java:14:    boolean validatedDnsPinning,
    ReactiveTransportCapabilities.java:16:    boolean serverSentEvents,
    ReactiveTransportCapabilities.java:17:    boolean cancellationReleasesConnection) {
    BlockingTransportCapabilities.java:19:    boolean validatedDnsPinning,

serverSentEventscancellationReleasesConnection 은 선언 줄 말고 나오는 곳이 없다. 39 의 주석이 validatedDnsPinning 에 대해 적은 상태가 이 둘에는 손대지 않은 채로 있다.

확인하지 못한 것

배포가 제공자 맵을 실제로 갈아 끼워 그 조합을 기동시키지 않았다. 확인한 것은 기동 검증기의 판정까지다.