--- kind: CASE slug: a11-f007-dns title: 혼동이라 적어 둔 이름이 아직 검사 문구에 있다 topic: http-client-and-resilience project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a11-f007-dns evidenceCapturedOn: 2026-09-02 body: case-a11-f007-dns.body.md assets: - key: a11-f007-dns file: ../../../final/evidence/rendered/a11-f007-dns.svg - key: a11-f007-dns-overloads file: ../../../final/evidence/rendered/a11-f007-dns-overloads.svg evidence: - ../../../final/evidence/raw/a11-f007-dns.txt - ../../../final/evidence/raw/a11-f007-dns-overloads.txt source: - 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#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" ::: 블로킹 쪽의 마지막 두 검사다. ```java 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: } ``` 반응형 쪽은 여기서 끝난다. ```java 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 을 붙들지 못한다. ```java 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" ::: ```text 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 고정이 없다는 문구다. 검사한 것은 다른 플래그다. 갈리는 것은 마지막 줄뿐이다. ## 지금 그 조합은 만들어지지 않는다 ```text 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 를 복사해도 오늘 바뀌는 판정은 없다. ## 막고 있는 것은 프로파일 검증기가 아니다 ```text 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")); ``` 이 검사가 이름으로 막는 것은 두 상수다. 전송이 하나 늘면 여기에는 걸리지 않는다. 오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 다른 데 있다. ```java @Bean @ConditionalOnMissingBean(name = "httpClientReactiveTransportProviders") Map httpClientReactiveTransportProviders( ObjectProvider meterRegistry, TlsMaterialProvider tlsMaterialProvider) { Map providers = new EnumMap<>(TransportType.class); providers.put( TransportType.REACTOR_NETTY, ``` 맵에 들어가는 전송이 하나다. 그리고 그 빈에는 빈 부재 조건이 붙어 있다. 배포가 자기 맵을 올리면 소스를 갈라내지 않고 다른 능력을 그 자리에 넣을 수 있다. ## 아직 아무도 읽지 않는 플래그 ```text 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, ``` `serverSentEvents` 와 `cancellationReleasesConnection` 은 선언 줄 말고 나오는 곳이 없다. 39 의 주석이 `validatedDnsPinning` 에 대해 적은 상태가 이 둘에는 손대지 않은 채로 있다. ## 확인하지 못한 것 배포가 제공자 맵을 실제로 갈아 끼워 그 조합을 기동시키지 않았다. 확인한 것은 기동 검증기의 판정까지다.