시간을 잊은 블록리스트

발행: (2026년 2월 24일 오후 03:40 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.

실제로 일어난 일

CVE‑2026‑27127이 오늘 Craft CMS에 릴리스되었습니다. 고위험 SSRF via DNS rebinding입니다. 권고문은 표준 보일러플레이트처럼 보이지만, 패치 노트의 한 세부 사항이 눈에 띄었습니다: 이 CVE는 동일 코드베이스의 이전 SSRF 수정인 CVE‑2025‑68437을 우회합니다. 이전 수정은 배포되었고, 펜테스터들이 서명했지만, 공격자는 여전히 이를 바로 통과할 수 있었습니다.

이는 단순 버그가 아니라 보안 검토를 통과한 카테고리 오류입니다.

원래 수정은 IP 차단 목록을 추가했습니다. 외부 HTTP 요청을 하기 전에 Craft는 대상 호스트명을 해결하고 IP를 거부 목록(AWS 메타데이터 169.254.169.254, GCP, Azure, RFC 1918 범위, 루프백 등)과 비교합니다. IP가 목록에 있으면 요청이 차단됩니다.

합리적이고 표준적인 관행. 하지만 잘못되었습니다.

취약한 로직(권고문에서 재구성)은 다음과 같습니다:

// Validation: DNS lookup #1
$ip = gethostbyname($hostname);
if (in_array($ip, $blocklist)) {
    return false; // blocked
}

// Request: DNS lookup #2 (inside Guzzle)
$response = $client->get($url);

두 번의 DNS 조회가 발생합니다: 하나는 검증용이고, 다른 하나는 HTTP 라이브러리(Guzzle) 내부에서 수행됩니다.

공격자가 DNS 서버를 제어하고 자신의 도메인에 TTL=0을 설정할 수 있습니다. 첫 번째 조회는 안전한 IP를 반환해 차단 목록 검사를 통과합니다. Guzzle이 실제 요청을 위해 동일한 호스트명을 다시 해결할 때쯤 DNS 레코드가 169.254.169.254로 변경됩니다. 요청은 AWS 메타데이터 엔드포인트에 도달해 자격 증명을 유출합니다. 차단 목록은 실제 목적지를 전혀 보지 못합니다.

이것은 DNS에 적용된 TOCTOU

Time‑of‑Check/Time‑of‑Use (TOCTOU)는 가장 오래된 버그 종류 중 하나입니다. 시간 T₁에 조건을 확인하고, 시간 T₂에 그 조건에 따라 동작을 수행하는데, 그 사이에 무언가가 변합니다. 고전적인 예는 파일 시스템 레이스입니다: 파일을 확인한 뒤 열고, 그 사이에 파일이 교체되는 경우입니다.

DNS 리바인딩은 다른 매체에서 발생하는 동일한 버그입니다. 확인되는 조건은 “이 호스트 이름이 안전한 IP로 해석되는가?”이며, 동작은 HTTP 요청입니다. 두 시점 사이의 간격은 공격자가 DNS 서버를 장악하고 서로 다른 질의에 서로 다른 응답을 반환할 수 있을 때 악용될 수 있습니다.

TTL=0인 경우, 리바인딩은 거의 즉시 발생합니다. 무력화할 캐시가 없으며, 윈도우는 마이크로초에서 밀리초 수준으로 매우 짧지만 협력 DNS 서버가 있으면 신뢰성 있게 악용할 수 있습니다.

유사한 패턴은 최소 2019년부터 버그 바운티 보고서(예: AWS 키를 유출하는 파이썬 웹훅 서비스)와 CVE‑2024‑28224(Ollama)에서도 등장했습니다. 이 실수가 계속 재발하는 이유는 수정이 겉보기엔 맞아 보이기 때문입니다: IP를 확인했으니 안전하다고 생각하게 되죠.

“Better Blocklist”가 도움이 되지 않는 이유

이 버그를 본 뒤의 본능적인 반응은 차단 목록을 확장하는 것입니다: 더 많은 범위를 추가하고, 두 번째 검증 단계를 수행하는 등. 하지만 그것은 잘못된 방향입니다.

아무리 포괄적인 차단 목록이라도 구조적인 문제를 해결하지 못합니다. 호스트 이름이 두 번 해석됩니다—한 번은 여러분이 제어하고, 또 한 번은 HTTP 라이브러리가 수행합니다. 이 두 해석이 별개로 이루어지는 한, DNS를 장악한 공격자는 각 쿼리에 서로 다른 응답을 반환할 수 있습니다.

차단 목록이 모든 클라우드 메타데이터 범위, 모든 사설 IP, 그리고 모든 루프백 주소를 포함하고 있더라도, 공격자의 DNS 서버는 여전히 다음을 할 수 있습니다:

  1. 검증 쿼리에는 안전한 IP를 반환한다.
  2. 라이브러리의 쿼리에는 악의적인 IP(예: 메타데이터 서비스)를 반환한다.

차단 목록은 실제로 중요한 해석과는 다른 해석을 검사하고 있습니다.

아키텍처 수준에서 해결하기

Craft 패치는 CURLOPT_RESOLVE를 사용합니다. 이는 libcurl 옵션으로, 요청이 진행되는 동안 호스트명을 특정 IP에 고정시킵니다. 흐름은 다음과 같습니다:

  1. 호스트명을 한 번만 해결합니다.
  2. 해당 IP를 블록리스트와 대조하여 검증합니다.
  3. curl에 “이 호스트명에 대해 IP를 사용하고, 다시 해결하지 말라”고 지시합니다.
  4. 요청을 수행합니다.

해결은 한 번만 일어나며, 라이브러리는 이후에 DNS 조회를 수행하지 않습니다.

또는 URL을 직접 IP로 바꾸고 원래 호스트명을 Host 헤더로 전달하는 방법도 있습니다. 원리는 동일합니다: DNS가 같은 답을 두 번 반환한다는 신뢰 대신, 실제 요청이 가는 IP를 직접 제어합니다.

일관되게 올바른 패턴은 다음과 같습니다:

신뢰 경계에서 해결하고, 검증한 뒤 고정한다.
독립적으로 해결을 수행할 수 있는 라이브러리에 호스트명을 다시 넘겨주지 마세요. 최신 SSRF 방지 라이브러리들은 모두 이 설계 결정을 공유하며, DNS 해결을 주변에서 한 번만 수행하는 작업으로 취급합니다.

나를 괴롭히는 부분

2025년 수정은 진정한 시도였습니다. 누군가 누락된 검증을 찾아내고 차단 목록을 작성한 뒤 배포했습니다. 보안 검토도 이루어졌고 통과했을 것으로 보입니다.

“이 IP가 안전해 보이나요?”는 잘못된 질문입니다. 올바른 질문은 “요청이 실제로 이 IP로 전송될까요?” 입니다. 이는 한 번만 해석하고 고정(pin)했을 때만 동일합니다. 2025년 수정은 잘못된 질문에—능숙하게—답했지만, HTTP 요청 중 DNS의 기본 모델을 간과했습니다.

특히 DNS를 내부적으로 처리하는 요청 라이브러리를 사용할 때 자체 코드에 SSRF 방어를 구현한다면 스스로에게 물어보세요:

  • 호스트명을 정확히 한 번만 해석했나요?
  • 그 해석을 실제 요청에도 동일하게 사용했나요?

두 질문 모두 “예”라고 답할 수 없다면, 여러분의 차단 목록은 단지 장식에 불과합니다.

Craft CMS 수정은 4.16.19와 5.8.23에 포함되었습니다. GraphQL 자산 생성이 활성화된 자체 호스팅 인스턴스를 운영 중이라면 지금 바로 업데이트하세요.

0 조회
Back to Blog

관련 글

더 보기 »

영향력 있는 해킹

소개: 저는 healthcare 분야에서 무언가를 구축하고 있습니다. 네, 그 말이 어떻게 들릴지 압니다. 이 분야는 규제가 매우 엄격하고, 위험도가 비정상적으로 높으며, 그리고 구축…

GPU 프로파일링 (CUDA) — GPU Flight 소개

작년에는 대학원 과정의 일환으로 Johns Hopkins University에서 GPU 프로그래밍 과목을 수강했으며, 그곳에서 CUDA 프로그래밍을 배웠습니다. 최종 프로젝트로는…