
SSRF 필터는 호스트네임 텍스트를 검사했지만, 실제 목적지는 나중에 DNS에 의해 결정되었습니다. 그 차이로 인해 공격자가 제어하는 웹훅 URL이 루프백, 메타데이터 및 사설 네트워크 대상에 도달할 수 있었습니다.
SSRF 필터는 호스트명 텍스트를 검사했지만, 실제 목적지는 나중에 DNS에 의해 결정되었습니다. 이 간격으로 인해 공격자가 제어하는 Webhook URL이 루프백, 메타데이터 및 사설 네트워크 대상에 도달할 수 있었습니다.
저는 오픈소스 챗봇 빌더인 Typebot을 검토하면서 간단한 보안 질문을 염두에 두고 이 문제를 발견했습니다.
SSRF 보호가 호스트명 텍스트를 검증하지만 실제 목적지는 나중에 DNS에 의해 결정된다면 어떻게 될까?
이 경우, 그 질문은 실제 버그로 이어졌습니다.
Typebot의 Webhook / HTTP 요청 블록에 대한 SSRF 보호는 다음만 검증했습니다.
요청을 허용하기 전에 DNS 확인을 하지 않았습니다.
즉, ssrf-repro.example 같은 호스트명은 검증 중에는 무해해 보일 수 있지만, 이후에 다음으로 확인될 수 있었습니다.
127.0.0.1169.254.169.254그리고 백엔드 HTTP 클라이언트가 여전히 해당 요청을 가져갔습니다.
이 문제는 CVE-2026-34207이 되었습니다.
Typebot: GitHub의 Typebot
CVE: CVE-2026-34207
수정 버전: 3.16.0
이는 널리 사용되는 오픈소스 챗봇 플랫폼인 에 영향을 미쳤습니다. 공식 사이트에서 Typebot은 전 세계 이 신뢰하는 것으로 소개됩니다. 또한 사이트는 과 을 광고합니다.

공격자가 제어하는 Webhook URL -> 호스트명이 리터럴 전용 SSRF 검증 통과 -> 허용 결정 전 DNS 확인 없음 -> 백엔드 HTTP 클라이언트가 호스트명을 내부 대상으로 확인 -> 서버 측 요청이 루프백/메타데이터/사설 네트워크에 도달 -> 응답 데이터가 실행 로그를 통해 사용 가능
Typebot은 챗봇 빌더입니다.
사용자가 다음을 수행할 수 있는 흐름을 만들 수 있습니다.
Webhook / HTTP 요청 블록을 통해 아웃바운드 HTTP 요청 트리거즉, 아웃바운드 요청 실행은 실제 보안 경계입니다.
여기서 중요한 질문은 Typebot이 Webhook 블록을 지원하는지 여부가 아니었습니다.
진짜 질문은 다음과 같았습니다.
SSRF 보호가 서버가 연결할 실제 대상을 검증하는가, 아니면 URL에 나타나는 호스트명 텍스트만 검증하는가?
이 경우, 먼저 텍스트 형식만 검증했습니다.
그것이 실수였습니다.
SSRF 방어는 매우 예측 가능한 방식으로 실패합니다.
대부분의 경우 흥미로운 실수는 다음과 같은 것이 아닙니다.
169.254.169.254 차단을 잊었다"localhost 차단을 잊었다"더 강력한 실수는 경계 실수입니다.
여기서 살펴볼 올바른 위치였습니다.
Typebot에는 이미 리터럴 메타데이터 IP, 루프백, 사설 범위 및 인코딩된 IP 트릭에 대한 SSRF 강화 로직이 있었습니다.
그러면 다음 질문이 명확해졌습니다.
호스트명이 문자 그대로 위험하지는 않지만, 나중에 위험한 대상으로 확인된다면?
정확히 그런 일이 일어났습니다.
근본 문제는 호스트명 텍스트 기반 대상 검증이지 확인된 IP 주소 기반이 아니었던 것입니다.
취약한 구현에서 validateHttpReqUrl()은 다음을 수행했습니다.
http: 및 https:만 허용metadata.google.internal, metadata.goog, metadata 및 localhost와 같은 리터럴 호스트명 짧은 목록 차단그것이 중요한 부분입니다.
호스트명이 다음과 같은 일반 값인 경우:
ssrf-repro.example
그러면 parseIPAddress(hostname)이 null을 반환하고 검증기가 거기서 멈췄습니다.
승인 전에 DNS 확인이 발생하지 않았습니다.
따라서 취약한 로직은 본질적으로 다음과 같이 축소되었습니다.
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
즉, 다음을 의미합니다.
버그의 나머지 절반은 실행 경로에 있었습니다.
executeHttpRequest()에서 Typebot은 먼저 검증을 실행한 다음 나중에 ky(request.url, ...)를 사용하여 실제 요청을 수행했습니다.
따라서 순서는 다음과 같았습니다.
그것이 전체 취약점입니다.
중요한 차이점은 신뢰 결정이 내려진 위치입니다.
많은 버그를 잘못 설명하면 작아 보입니다.
이것을 다음과 같이 설명한다면:
"호스트명 필터가 불완전했다"
품질 문제처럼 들립니다.
그것이 실제 문제가 아닙니다.
실제 문제는 다음과 같습니다.
이는 표면적인 필터링 약점이 아닙니다.
이는 신뢰 경계 실패입니다.
그리고 HTTP 실행기가 실행 로그에 응답 데이터를 기록했기 때문에, 가장 강력한 경우에도 문제가 눈에 띄지 않는 것이 아니었습니다.
따라서 이것은 단순히:
가 아니라:
그것이 실제 SSRF 취약점입니다.
두 가지 다른 것을 증명했기 때문에 두 가지 증명 계층을 사용했습니다.
첫 번째 PoC는 근본 원인을 깔끔하게 분리했습니다.
다음을 수행하는 작은 로컬 하네스를 사용했습니다.
127.0.0.1에서 루프백 HTTP 서버 시작http://ssrf-repro.example:18080/...와 같은 URL 검증ssrf-repro.example이 127.0.0.1로 확인되도록 함이는 정확한 결함을 입증했습니다.
캡처된 출력은 다음을 보여주었습니다.
이는 검증 간격을 직접 증명했습니다.
두 번째 PoC는 중요한 실제 기능 경로를 통해 버그를 보여주었습니다.
가장 간단한 재현 방법은 다음과 같습니다.
Webhook 블록 생성대표적인 블록은 다음과 같습니다.
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
다음과 같은 hosts 파일 항목을 사용하여:
127.0.0.1 ssrf-repro.example
검증기는 여전히 URL을 수락했습니다. 왜냐하면:
ssrf-repro.example이 blockedHostnames에 없음localhost가 아님parseIPAddress("ssrf-repro.example")이 null 반환그런 다음 백엔드 HTTP 클라이언트가 호스트명을 127.0.0.1로 확인하고 여전히 연결했습니다.
이는 전체 주장을 확립했습니다.
첫 번째 PoC는 근본 원인을 증명합니다.
두 번째 PoC는 제품 영향을 증명합니다.
그 구분이 중요합니다.
다음만 보여준다면:
"이 필터가 호스트명을 수락한다"
충분히 보여준 것이 아닙니다.
다음만 보여준다면:
"내부 요청이 발생했다"
왜 그런지 분리하지 않았습니다.
더 강력한 보고서는 다음과 같습니다.
그것이 전체 이야기입니다.
이 문제는 합리적으로 높음(High) 으로 분류되었습니다.
권고 분류는 다음과 같았습니다.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
이는 타당합니다.
이것 자체만으로는 인증되지 않은 인터넷 전체 SSRF가 아니었습니다.
필요한 권한은 낮음(Low) 이었습니다. 독립형 버그는 Webhook / HTTP 요청 블록을 구성하거나 트리거할 수 있는 행위자가 필요했기 때문입니다.
그러나 해당 경계 내에서 영향은 심각했습니다.
이는 그 자체로 강력한 SSRF 문제입니다.
그리고 도달 가능성을 넓히는 다른 버그와 결합되면 훨씬 더 위험해집니다.
일부 사람들은 인증된 SSRF를 보고 즉시 과소평가합니다.
그것은 실수입니다.
진짜 질문은 다음과 같은 것이 아닙니다.
"공격자가 로그인했습니까?"
진짜 질문은 다음과 같습니다.
"그 공격자가 서버가 보안 모델이 금지하기로 되어 있는 곳에 연결하도록 할 수 있습니까?"
여기서 답은 '예'였습니다.
검증기는 보호한다고 주장했습니다.
그러나 호스트명 확인 간격으로 인해 동일한 대상이 다른 표현을 통해 다시 들어올 수 있었습니다.
이것이 바로 CVE를 받을 자격이 있는 버그 종류입니다.
실제 경계를 수정했기 때문에 수정은 견고했습니다. 단순한 증상이 아닙니다.
패치된 구현에서 validateHttpReqUrl()은 요청을 승인하기 전에 호스트명을 확인하도록 변경되었습니다.
이제 검증기는 다음을 수행합니다.
node:dns/promises에서 lookup 가져오기이는 신뢰 결정을 다음과 같이 변경하기 때문에 올바른 수정입니다.
에서
로 변경되었습니다.
이는 처음부터 존재했어야 할 보안 속성입니다.
또한 수정 사항은 다음에 대한 회귀 테스트를 추가했습니다.
이것이 실제 SSRF 수정에서 원하는 종류의 강화입니다.
이 문제는 Typebot 3.16.0에서 해결되었습니다.
이 문제는 GitHub의 보안 보고 흐름을 통해 비공개로 보고되었습니다.
보고서에는 다음이 포함되었습니다.
validateHttpReqUrl()의 근본 원인executeHttpRequest()의 다운스트림 실행 경로유지 관리자가 문제를 수락하고 검증 로직을 수정했으며, 취약점은 나중에 다음과 같이 게시되었습니다.
CVE-2026-34207
수정 사항은 Typebot 3.16.0에서 릴리스되었습니다.
핵심 교훈은 간단합니다.
보안 결정은 네트워크 스택이 실제로 사용할 동일한 표현에서 이루어져야 합니다.
당연해 보입니다.
그러나 많은 SSRF 보호가 정확히 그 규칙을 따르지 않기 때문에 실패합니다.
그것으로 충분합니다.
이 버그는 또한 SSRF 검토 일반에 대해 중요한 점을 강화합니다.
실제 대상을 확인하고 검증하지 않으면 SSRF 필터가 여전히 불완전합니다.
그것이 진짜 핵심입니다.
이 취약점은 화려한 페이로드에 관한 것이 아니었습니다.
올바른 경계 질문을 하는 것에 관한 것이었습니다.
Typebot에서 SSRF 검증기는 먼저 호스트명 텍스트를 확인했습니다. 실제 목적지는 나중에 DNS에 의해 결정되었습니다. 네트워크 클라이언트는 확인된 주소를 따랐습니다.
그 간격이 버그였습니다.
그것이 이 문제가 CVE-2026-34207이 된 이유입니다.
Typebot 3.16.0에서 수정되었습니다.