09. DNS Rebinding: origin과 endpoint의 경계
DNS Rebinding은 공격자가 소유한 hostname의 DNS 응답을 바꿔, 브라우저가 처음에는 외부 서버에서 코드를 받고 나중에는 같은 hostname으로 내부망이나 loopback 서비스에 연결하게 만드는 공격이다. 핵심은 DNS 자체를 해킹하는 것이 아니라 웹의 origin identity와 실제 network endpoint 사이의 간극을 이용하는 데 있다.
흔히 “동일 출처 정책(Same-Origin Policy, SOP) 우회”라고 부르지만, 브라우저가 SOP 검사를 생략하는 것은 아니다. RFC 6454의 origin은 대략 (scheme, host, port) tuple이며 여기에 resolved IP는 없다. DNS가 가리키는 endpoint가 달라져도 URL의 세 요소가 같으면 origin 비교 결과도 같다.
이 문서의 실습은 DNS 조회, socket 연결, 브라우저 요청을 전혀 수행하지 않는 고정 데이터 모델이다. 허가받은 로컬 환경 밖에서 내부 주소 탐색이나 서비스 접근을 시도하지 않는다.
위협 모델
공격이 성립하려면 다음 조건이 함께 맞아야 한다.
- 공격자가 domain, authoritative DNS 응답, 최초 웹 콘텐츠를 제어한다.
- 피해 브라우저가 최초 endpoint에서 받은 active content를 계속 실행한다.
- 브라우저·운영체제·proxy의 cache와 connection reuse가 끝나 hostname을 다시 해석하고 새 연결을 만든다.
- 다음 DNS 응답이 피해자 위치에서 접근 가능한 private, link-local 또는 loopback endpoint를 가리킨다.
- 대상 서비스가 같은 scheme과 port에서 응답하며, 예상하지 않은
Host·Origin·TLS identity를 거부하지 않는다. - 브라우저의 Local Network Access 같은 추가 보호가 요청을 막지 않는다.
짧은 TTL 하나만으로 성공이 보장되지는 않는다. 브라우저와 운영체제의 DNS cache, HTTP keep-alive, HTTP/2 connection coalescing, proxy, 여러 A·AAAA 응답이 실제 재해석 시점을 바꾼다. 따라서 특정 브라우저의 고정된 “재바인딩 대기 시간”을 일반 규칙처럼 외우는 것은 잘못이다.
공격 흐름
예시 URL을 http://lab.example.test:8080/이라고 하자.
1. lab.example.test -> 192.0.2.10
브라우저가 외부 endpoint에서 문서와 script를 받는다.
2. 문서의 origin = (http, lab.example.test, 8080)
script는 이 origin의 권한으로 실행된다.
3. 새 연결 시 lab.example.test -> 127.0.0.1
URL과 origin tuple은 그대로지만 network endpoint가 달라진다.
4. request target = 127.0.0.1:8080
Host = lab.example.test:8080
대상 서비스가 이 요청을 받아들이면 script 입장에서는 여전히 자신의 origin에 보낸 요청이다. 이 때문에 응답을 읽는 단계에서 일반적인 cross-origin CORS 검사가 근본 경계가 되지 못한다. 브라우저는 hostname의 소유 주체가 endpoint 변경 전후에 달라졌는지를 origin tuple만으로 표현할 수 없다.
2007년 Jackson 등의 논문은 이 과정을 “서로 다른 주체가 통제하는 network resource를 하나의 origin으로 묶는 혼동”으로 설명한다. 공격자는 자신의 domain에 대해 정상적인 authoritative answer를 제공하므로 DNS cache poisoning이 필요하지 않고, 자신이 소유한 zone을 올바르게 서명할 수도 있어 DNSSEC만으로는 막히지 않는다.
비슷해 보이는 문제와 구분하기
CORS misconfiguration과 다르다
CORS는 다른 origin이 응답을 읽도록 허용하는 opt-in 메커니즘이다. DNS Rebinding의 두 요청은 hostname이 같아 same-origin으로 평가될 수 있으므로, Access-Control-Allow-Origin을 제거하는 것만으로는 이 경계를 복구하지 못한다.
반대로 DNS Rebinding 없이 외부 origin이 내부 API의 응답을 읽는 문제라면 CORS 설정이 직접 원인일 수 있다. 또한 응답을 읽지 않아도 상태 변경이 가능한 endpoint는 CSRF 방어가 별도로 필요하다.
DNS poisoning과 다르다
DNS poisoning은 피해자가 신뢰하는 domain의 해석 결과를 오염시킨다. DNS Rebinding에서는 공격자가 자신의 domain에 유효한 응답을 하므로 resolver나 cache를 탈취할 필요가 없다.
SSRF와 행위 주체가 다르다
브라우저형 DNS Rebinding에서는 사용자의 브라우저가 내부망으로 들어가는 deputy다. SSRF에서는 서버의 URL fetcher가 deputy다. 서버 측에서도 URL 검증 시점과 실제 연결 시점의 DNS 결과가 달라지는 rebinding/TOCTOU 문제가 생길 수 있지만, 연결 주체와 방어 위치가 다르다.
안전한 로컬 모델
아래 코드는 RFC 5737의 문서 전용 주소와 loopback 주소를 고정 배열에 넣어 origin은 그대로인데 endpoint만 바뀌는 상태를 모델링한다. DNS, HTTP, JavaScript, socket API를 사용하지 않으므로 어떤 host에도 packet을 보내지 않는다.
from dataclasses import dataclass
from urllib.parse import urlsplit
@dataclass(frozen=True)
class Origin:
scheme: str
host: str
port: int
def origin_of(url: str) -> Origin:
parsed = urlsplit(url)
if parsed.scheme not in {"http", "https"} or parsed.hostname is None:
raise ValueError("absolute HTTP(S) URL required")
default_port = 443 if parsed.scheme == "https" else 80
port = parsed.port if parsed.port is not None else default_port
return Origin(parsed.scheme, parsed.hostname, port)
url = "http://lab.example.test:8080/status"
document_origin = origin_of(url)
# 실제 resolver가 아니라 순서가 고정된 학습용 데이터다.
resolved_endpoints = ("192.0.2.10", "127.0.0.1")
first_endpoint, rebound_endpoint = resolved_endpoints
assert origin_of(url) == document_origin
assert rebound_endpoint != first_endpoint
print(f"origin: {document_origin}")
print(f"first endpoint: {first_endpoint}:8080")
print(f"rebound endpoint: {rebound_endpoint}:8080")
print(f"origin changed: {origin_of(url) != document_origin}")
print(f"endpoint changed: {first_endpoint != rebound_endpoint}")
출력은 다음과 같다.
origin: Origin(scheme='http', host='lab.example.test', port=8080)
first endpoint: 192.0.2.10:8080
rebound endpoint: 127.0.0.1:8080
origin changed: False
endpoint changed: True
이 모델이 증명하는 것은 identity mismatch뿐이다. 실제 공격 가능성을 증명하지는 않는다. 현실에서는 DNS cache, transport connection, browser policy, TLS, service authentication이 모두 추가 변수다.
허가된 환경에서 검증할 것
자신이 소유한 브라우저와 두 endpoint만 사용한 lab에서는 다음 증거를 분리해 기록한다.
- 최초 문서의 URL과 origin tuple
- 각 요청이 실제 연결한 remote address
- DNS query 시각과 A·AAAA answer
- connection reuse 여부와 새 연결 시점
- 대상 서비스가 받은
Host,Origin, method, authentication 상태 - 브라우저가 요청을 차단했다면 console에 기록된 정확한 정책 이름
TTL이 짧았다는 사실이나 DNS answer가 바뀌었다는 사실만으로 취약점이 입증되지는 않는다. 동일한 origin의 active content가 다른 endpoint에 실제로 도달했고, 서비스의 보안 경계를 넘었다는 증거가 필요하다. 운영망 주소 탐색이나 제3자 장비 접근은 이 검증 범위가 아니다.
방어: 내부망을 authentication으로 착각하지 않는다
한 계층에만 의존하면 browser·DNS·network 구현 차이로 방어가 무너질 수 있다. 대상 서비스에서 시작해 browser와 resolver로 바깥쪽 방어를 겹친다.
1. 서비스가 자신의 identity를 검증한다
- reverse proxy와 application 양쪽에서 허용된
Host또는 HTTP/2·3:authority를 명시적으로 관리하고, 알 수 없는 값은 routing 전에 거부한다. - proxy가 외부
Host를 내부의 신뢰된 hostname으로 무조건 덮어쓰지 않는지 확인한다. 최초 ingress에서 검증한 값을 이후 계층이 보존해야 한다. - 가능하면 HTTPS를 사용하고 대상 서비스의 의도된 hostname에 유효한 certificate만 허용한다. 공격자 hostname으로 접속한 TLS client는 certificate identity 불일치에서 실패해야 하며, 사용자가 경고를 우회하게 해서는 안 된다.
- 모든 민감 기능에 authentication과 authorization을 적용한다. source IP, loopback, 사설망 도달 가능성은 credential이 아니다.
- 요청의
Origin이 설정에 고정된 service origin allowlist와 정확히 일치하는지 검증한다. allowlist를 요청의Host에서 동적으로 만들면 두 검사가 함께 우회될 수 있다.Origin은 없는 client도 있으므로 authentication을 대신하지 않는다. - CSRF token, Fetch Metadata, 안전한 method는 일반적인 cross-site 요청을 줄이는 별도 방어로 유지한다. 그러나 rebinding script는 same-origin으로 보일 수 있어
Sec-Fetch-Site: same-origin을 보내고 token 응답도 읽을 수 있으므로, 이 장치들을 DNS Rebinding의 직접 방어로 계산하지 않는다. - 민감한 관리 응답을 인증 없는
GET으로 제공하지 않는다.
loopback에만 bind하면 LAN의 직접 접근 면적은 줄지만 브라우저도 같은 host에서 loopback에 접근하므로 DNS Rebinding 자체의 완전한 방어는 아니다. Host 검증과 인증이 여전히 필요하다.
2. 브라우저의 local-network 경계를 보조 방어로 사용한다
Local Network Access(LNA)는 public context가 local 또는 loopback endpoint에 연결할 때 사용자 permission을 요구하고, 그 permission 요청 능력을 secure context로 제한하는 WICG browser proposal이다. 따라서 앞의 http://lab.example.test 모델만 보고 실제 browser에서 permission prompt가 나타난다고 추론할 수 없다. mixed-content 정책과 구현 상태에 따라 prompt 전에 차단될 수도 있다.
Chrome의 2025년 발표는 Chrome 138에서 flag로 켜는 opt-in test와 당시 fetch, subresource, subframe 지원 및 WebSocket 등의 제한을 설명한 시점별 구현 snapshot이다. 현재 모든 Chrome이나 다른 browser에 같은 정책이 배포됐다는 근거가 아니다. 배포 대상 browser matrix에서 실제 version과 transport별 동작을 확인하고, LNA는 서비스 인증 위의 defense-in-depth로 취급한다.
3. resolver와 network boundary에서 재바인딩을 거른다
- 조직 resolver는 public DNS name이 private, link-local, loopback 주소를 반환하는 응답을 차단한다.
- client가 임의의 외부 resolver로 우회하지 못하도록 DNS egress 정책과 암호화 DNS 정책을 함께 설계한다.
- A뿐 아니라 AAAA, IPv4-mapped IPv6, CNAME chain 전체에 같은 판정을 적용한다.
- 의도적인 split-horizon DNS와 public-to-private failover domain은 좁은 allowlist로 예외 처리하고 기록한다.
이는 firewall 우회 경로를 줄이지만 대상 서비스의 인증을 대체하지 않는다. 다른 resolver, VPN, proxy, 이미 내부에 있는 공격 origin 같은 경로가 남을 수 있다.
4. 서버 측 URL fetcher는 연결 직전 endpoint를 고정한다
SSRF 방어에서는 hostname 문자열만 검사하지 않는다.
- 허용된 scheme·port·hostname을 먼저 제한한다.
- 모든 A·AAAA 결과를 정규화해 private, loopback, link-local, multicast, reserved range를 거부한다.
- 검증한 IP로 실제 connection을 만들고, HTTP client가 그 사이 hostname을 다시 해석하지 않게 한다.
- TLS certificate는 원래 허용한 hostname으로 검증한다.
- redirect마다 URL과 DNS 결과를 처음부터 다시 검사한다.
“요청 직전 한 번 더 resolve”만 하고 client가 다시 resolve하도록 두면 check와 use 사이의 경쟁 조건이 남는다. 이 방어는 browser형 공격이 아니라 서버형 deputy에 적용된다는 점도 구분한다.
자주 실패하는 방어
- CORS만 끈다 — rebinding 뒤에도 URL hostname이 같으면 same-origin 요청일 수 있다.
- DNSSEC만 켠다 — 공격자는 자신이 소유한 zone의 변경되는 answer를 정상적으로 서명할 수 있다.
- 짧은 TTL을 무시하고 오래 cache한다 — 위험을 줄일 수는 있지만 dynamic DNS, failover, 여러 network stack 때문에 단독 보장이 되지 않는다.
- firewall 안쪽 서비스는 인증하지 않는다 — 브라우저가 이미 firewall 안쪽의 network vantage point를 가진다.
127.0.0.1만 차단한다 — RFC 1918, link-local, IPv6 loopback·ULA, IPv4-mapped IPv6 등 다른 local address space가 남는다.Host만 검사한다 — 중요한 방어지만 trusted hostname을 가진 내부 attacker, proxy rewriting, non-HTTP protocol까지 해결하지는 않는다.
방어 검증 체크리스트
- 알 수 없는
Host와:authority를 최초 ingress에서 거부하는가? - 내부·loopback 서비스에도 독립적인 authentication과 authorization이 있는가?
- 예상
Originallowlist가 요청의Host가 아닌 신뢰된 설정에서 만들어지는가? - CSRF token과 Fetch Metadata를 DNS Rebinding의 단독 방어로 계산하지 않았는가?
- HTTPS certificate를 의도한 service hostname으로 검증하는가?
- public-to-local 요청에 대한 browser별 LNA 동작을 실제 transport별로 확인했는가?
- resolver filter가 A·AAAA·CNAME과 IPv4-mapped IPv6를 모두 처리하는가?
- server-side fetcher가 검증한 IP로 연결하고 redirect마다 재검증하는가?
- 회귀 테스트가 예상 hostname은 허용하고 attacker-controlled hostname은 거부하는가?
관련 문서
- SSRF 완벽 가이드 — server-side deputy와 outbound URL validation
- OAST로 SSRF를 빠르게 검증하는 방법 — 허가된 범위에서 DNS·HTTP 관찰 증거를 남기는 방법
- CORS 설정 오류와 실전 익스플로잇 — cross-origin opt-in 문제와 same-origin rebinding의 차이
- Host 헤더 공격 빠른 체크 — proxy와 application의 hostname trust boundary
참고 자료
- RFC 6454 — The Web Origin Concept — origin tuple과 DNS 의존성에 대한 표준
- Jackson et al., “Protecting Browsers from DNS Rebinding Attacks” (CCS 2007) — 공격 모델과 browser·resolver·server 방어를 체계화한 논문
- RFC 9110 §7.2 — Host and :authority — HTTP target authority 전달 규칙
- WICG Local Network Access — public-to-local·loopback 요청의 permission 기반 browser proposal
- Chrome, “A new permission prompt for Local Network Access” — Chrome의 LNA 도입 범위와 알려진 제한
- RFC 5737 — IPv4 Address Blocks Reserved for Documentation — 로컬 모델의
192.0.2.10이 문서 전용 주소임을 정의 - RFC 2606 — Reserved Top Level DNS Names — 로컬 모델의
.test가 시험용으로 예약된 domain임을 정의