은탄환은 없다: 민속과 현대적 의미
“은탄환(no silver bullet)”이라는 표현은 유럽 민속에서 유래했으며, 은으로 만든 탄환만이 늑대인간이나 뱀파이어 같은 초자연적 존재를 물리칠 수 있다고 믿었습니다. 1816년 월터 스콧의 _Tales of My Landlord_에서 처음 문서화되었고, 1765년의 제보당의 야수 사건 등에서도 마지막 수단으로 은탄환이 언급됩니다. 시간이 흐르며 이 표현은 “복잡한 문제에 단 하나의 간단한 해결책은 없다”는 의미로 확장되었고, 소프트웨어 공학에서는 프레드 브룩스의 1986년 에세이로 널리 알려졌습니다. 이 글은 SSRF 방어에도 같은 교훈을 적용합니다: 만능 해결책에 의존하지 말고, 단순한 속설이나 임시방편이 아닌, 근본적인 원인과 구조적 대책을 고민해야 합니다.
“안전한” SSRF 패치의 한계: 고급 우회와 다계층 방어 전략
인트로: SSRF란 무엇이고, 왜 위험한가?
서버사이드 요청 위조(SSRF)는 공격자가 서버로 하여금 의도하지 않은 위치로 HTTP 요청을 보내도록 속이는 취약점입니다. 일반적인 SSRF 공격에서 공격자는 서버 코드가 가져올 URL이나 주소를 입력하지만, 예상한 외부 리소스 대신 서버가 내부 서비스나 보호된 엔드포인트에 접근하게 만듭니다. 이로 인해 공격자는 내부 네트워크를 스캔하거나, 데이터베이스 엔드포인트에 접근하거나, AWS EC2 토큰과 같은 클라우드 인스턴스 메타데이터를 탈취할 수 있습니다. 이러한 위험성 때문에 SSRF는 OWASP Top 10(2021)에 포함되어 있으며, 개발자들에게 점점 더 큰 위협이 되고 있습니다.
“그냥
startsWith('https://trusted.com')
만 쓰면 SSRF 막을 수 있어요!”
— StackOverflow에서 자주 보이는(하지만 실패하는) 조언
이처럼 SSRF의 위험성 때문에 많은 개발자들은 빠른 패치 방법에 의존합니다. 하지만 위 인용구에서 보듯, 선의의 조언이 오히려 불완전하거나 우회 가능한 방어로 이어지는 경우가 많습니다. 온라인에는 URL 필터링, 도메인 허용 리스트 등으로 SSRF를 “해결”했다고 주장하는 코드와 답변이 넘쳐나지만, 실제로는 고급 우회 기법에 취약한 경우가 대부분입니다.
이 글에서 다루는 내용:
- “안전한” SSRF 패치가 실전에서 왜 자주 실패하는지
- 공격자가 창의적인 페이로드로 어떻게 방어를 우회하는지
- Node.js(Express + Axios) 환경에서 실전적이고 계층적인 SSRF 방어 구현법
- 실습 가능한 SSRF Defense Lab과 테스트 전략
글을 끝까지 읽으면, 불완전한 SSRF 패치를 구별하고, 고급 우회 기법을 이해하며, 실제 공격에도 견디는 다계층 방어 전략을 직접 구현할 수 있게 됩니다. 보안은 반복적 테스트와 지속적 강화가 필요하지만, 올바른 접근법을 익힌다면 공격자보다 한발 앞서 나갈 수 있습니다.
흔히 쓰이는 SSRF 패치 방법과 그 한계
SSRF 취약점을 빠르게 막으려는 개발자들은 겉보기에 효과적으로 보이는 간단한 패턴을 자주 적용합니다. 대표적인 SSRF 방어 패턴은 다음과 같습니다:
- URL 접두어나 호스트명 일부로 허용 리스트 구성 – 예: 특정 도메인으로 시작하는 URL만 허용
- 기본적인 호스트네임 검증 – 예: 입력받은 URL을 파싱해 호스트네임이 기대값과 일치하는지 확인
- 문자열로 사설 IP 차단 – 예:
127.0.0.1
이나localhost
가 포함된 URL 거부 - 스킴 필터링 – 예:
http://
또는https://
만 허용, FTP나 file 등은 차단 - 리다이렉트 미허용 – 실제로는 강제하지 않고 “안 따라간다”고 가정하는 경우도 많음
- 악의적 패턴에 대한 피상적 체크 – 예:
file://
이 포함된 URL 차단, 16진수/인코딩된 IP 탐지 시도 등
이론적으로는 그럴듯해 보이지만, 공격자들은 이런 필터를 우회할 수 있는 다양한 트릭을 개발해왔습니다. 몇 가지 대표적인 허술한 SSRF 방어의 함정과 실제 우회 사례를 살펴보겠습니다:
1. 접두어 매칭 기반의 허술한 도메인 허용 리스트
가장 흔한 방식은 특정 신뢰 도메인 또는 베이스 URL로만 요청을 허용하는 것입니다. 예를 들어 아래와 같은 코드가 있을 수 있습니다:
// ❌ 취약한 SSRF 체크 (접두어 기반 허용 리스트)
if (!userUrl.startsWith(allowedBaseUrl)) {
throw new Error("URL not allowed");
}
겉보기에 단순합니다. 사용자가 입력한 URL이 https://trusted.example.com
으로 시작하지 않으면 차단합니다. 문제는: URL 문자열 매칭은 교묘한 포맷 변형에 쉽게 속을 수 있다는 점입니다. 공격자들은 @
표기법을 활용해 이런 체크를 우회합니다. HTTP URL에서 @
앞은 인증(userinfo) 정보로 간주되고, 실제 호스트는 @
뒤입니다. 예시:
- 정상 URL:
https://trusted.example.com/path/file.txt
- 공격 URL(
@
활용):https://trusted.example.com@evil.attacker-site.com/path/file.txt
겉으로 보면 공격 URL도 https://trusted.example.com
으로 시작하므로 startsWith()
체크를 통과합니다. 실제로는 호스트네임이 evil.attacker-site.com
이 되어 서버가 공격자 도메인에 요청을 보냅니다. 즉, SSRF 우회 성공입니다.
실제 사례: 오픈소스 ChatGPT-Next-Web 프로젝트도 API 엔드포인트에 접두어 기반 허용 리스트를 적용한 SSRF 패치를 썼다가 뚫렸습니다. 입력 URL이 허용된 호스트 접두어(예: 신뢰 WebDAV 서비스 URL)로 시작하는지만 검사했는데, 코드가 도메인 뒤 구분자를 강제하지 않아 공격자가 허용 접두어 뒤에 임의 문자열을 붙여 악의적 도메인으로 만들 수 있었습니다. 예를 들어, https://webdav.yandex.com
이 허용된 경우 공격자는 https://webdav.yandex.com.attacker.tld/evil
을 제출해 .startsWith()
를 통과시키고 실제로는 attacker.tld
로 요청을 보냈습니다. 이런 단순 접두어 체크는 악의적 호스트명에 신뢰 문자열을 포함시키는 것만으로 손쉽게 우회됩니다.
2. 얕은 호스트네임 검증과 서브도메인 트릭
조금 더 나은 방법은 URL을 실제로 파싱해서 호스트네임을 확인하는 것입니다. 예를 들어 Node의 url.parse()
나 WHATWG URL
API로 호스트네임을 추출해 허용 리스트와 비교할 수 있습니다:
const parsed = new URL(userUrl);
if (parsed.hostname !== "trusted.example.com") {
throw new Error("Hostname not allowed");
}
이렇게 하면 @
문제는 해결됩니다. https://trusted.example.com@evil.com
의 경우 parsed.hostname
은 evil.com
이기 때문입니다. 하지만 호스트네임 문자열 비교에도 허점이 있습니다. 서브도메인, 유사 도메인 문제입니다. 특정 호스트만 허용하려면 정확히 일치하는지(또는 엄격한 패턴)를 체크해야 합니다. 공격자는 허용 문자열을 포함한 도메인을 등록할 수 있습니다. 예시:
- 허용 호스트:
api.mycompany.com
- 공격자 등록:
api.mycompany.com.evil.org
(evil.org
의 서브도메인)
endsWith("api.mycompany.com")
이나 문자열 포함 체크만 하면 속을 수 있습니다. .mycompany.com
체크도 really-trusted.mycompany.com.evil.org
같은 도메인에 우회당할 수 있습니다. 위 ChatGPT-Next-Web 사례가 서브도메인 접두어 악용의 대표적 예입니다. 호스트 검증을 반드시 앵커링(전체 일치 또는 엄격한 패턴)해야 하며, 서브도메인을 허용한다면 *.example.com
처럼 점(.) 경계까지 고려해야 합니다(example.com.evil.com
은 절대 허용 X).
또 한 가지 자주 간과되는 점이 DNS 해석입니다. URL의 호스트네임이 멀쩡해 보여도 실제로 기대한 IP로 해석되는지 보장할 수 없습니다. DNS rebinding 공격에서는 공격자가 도메인을 소유하고 DNS 응답을 조작할 수 있습니다. 서버가 parsed.hostname
을 허용 리스트와 비교해도, 예를 들어 외부 호스트만 허용하는 필터라면 공격자 도메인이 1차 검증 때는 정상(public IP)으로, 실제 요청 때는 사설 IP(127.0.0.1 등)로 응답하게 만들 수 있습니다. 즉, DNS 해석 결과를 반드시 한 번만 얻어 그 IP를 검증하고, 가능하다면 그 IP로 직접 요청을 보내야 합니다. 여러 번 연결이 필요하다면 매번 검증해야 합니다.
마지막으로 대체 IP 표기법도 주의하세요. 코드에서 127.0.0.1
이나 localhost
만 막으면 공격자는 2130706433
(10진수, 0x7F000001
), IPv6 루프백 ::1
등으로 우회할 수 있습니다. 견고한 방어는 반드시 주소를 정규화해 검증해야 하며, IP 문자열 매칭에만 의존해선 안 됩니다.
3. 불충분한 리다이렉트 처리
HTTP 리다이렉트는 이를 고려하지 않은 SSRF 필터를 완전히 무력화할 수 있습니다. 대부분의 HTTP 클라이언트(Axios, requests 등)는 기본적으로 리다이렉트를 자동으로 따라갑니다. 즉, 백엔드가 301/302 응답을 받으면 클라이언트가 새 위치로 투명하게 요청을 보냅니다. SSRF 방어가 초기 URL만 검사한다면, 공격자는 중간 사이트를 경유해 손쉽게 우회할 수 있습니다.
예를 들어, 앱에서 https://cdn.safe-files.com
만 다운로드 허용한다고 가정합시다. 공격자는 해당 CDN(또는 허용 도메인)에 오픈 리다이렉트 취약점이 있거나, 302.r3dir.me
같은 SSRF 테스트용 리다이렉트 서비스를 사용합니다. 공격자는 허용 도메인으로 요청을 보내면 바로 악의적/내부 주소로 리다이렉트되도록 만듭니다. 코드는 요청 URL에 허용 도메인이 있으니 통과시키지만, 서버는 리다이렉트를 따라가 금지된 대상에 도달합니다. 결국 서버가 내부 자원에 요청을 보내 SSRF가 성공합니다.
실전 예시로 302.r3dir.me
서비스를 활용할 수 있습니다. 이 서비스는 URL 쿼리에 지정된 대상으로 리다이렉트해 SSRF 테스트를 도와줍니다. 예를 들어, 타깃이 AWS EC2 메타데이터 URL http://169.254.169.254/latest/meta-data/iam/security-credentials/
라면, 공격자는 다음과 같이 요청할 수 있습니다:
https://your-allowed-cdn.com@302.r3dir.me/--to/?url=http://169.254.169.254/latest/meta-data/iam/
이 URL은 @
트릭(접두어 체크 우회)과 내부 AWS 메타데이터로의 리다이렉트를 모두 포함합니다. 서버가 이를 따라가면 응답으로 AWS 자격증명이 노출됩니다! 실제 테스트에서는 https://cdn.example.com@302.r3dir.me/--to/?url=http://intranet.com/
과 같은 페이로드로 리다이렉트 기반 우회를 시뮬레이션합니다. 적절한 방어가 없다면 이런 요청이 허용 리스트를 뚫고 최종 타깃에 도달할 수 있습니다. 실제로 302.r3dir.me
(또는 유사 리다이렉트 서버)를 활용해 허술한 SSRF 방어를 우회, 내부 네트워크와 메타데이터 엔드포인트에 접근한 사례가 다수 보고되었습니다.
더 나쁜 점은 스킴(프로토콜) 강제도 리다이렉트로 우회될 수 있다는 것입니다. 일부 개발자는 보안을 위해 https://
만 허용하지만, 리다이렉트를 제어하지 않으면 공격자가 처음에는 https://
로 시작해도 이후 http://
로 바꿀 수 있습니다. 대부분의 HTTP 라이브러리는 HTTPS→HTTP 리다이렉트를 별다른 설정 없이 따라갑니다. Leviathan Security의 연구처럼, 이 트릭으로 공격자는 결국 내부 HTTP 서비스(많은 백엔드/클라우드 메타데이터 엔드포인트가 HTTP 사용)에 도달할 수 있습니다. 리다이렉트 제한이 없다면 “HTTPS만 허용” 규칙은 첫 홉에만 적용되고, 이후는 무방비입니다.
4. 기타 고급 우회 기법
접두어, 서브도메인, 리다이렉트 외에도 SSRF 필터를 우회하는 창의적 방법이 많습니다:
- IP/도메인 혼합 포맷: 일부 브라우저/클라이언트는
http://127.0.0.1.xip.io
나http://[::ffff:127.0.0.1]
같은 URL을 허용합니다. 호스트네임이나 IPv6처럼 보이지만 실제로는 IPv4 루프백입니다. 필터가 “127.0.0.1”만 막으면 이런 우회가 가능합니다. - HTTP 헤더 인젝션(드묾): 아주 드물지만, 사용자 입력이 HTTP 요청에 직접 연결될 때 공격자가 헤더 구분자를 삽입해 요청을 변조할 수 있습니다. SSRF 우회라기보다는 입력 파싱 취약점이지만, 반드시 라이브러리로 요청을 생성해야 함을 상기시킵니다.
- 대체 스킴(Gopher, File): 대부분의 SSRF 패치는 HTTP/HTTPS만 신경쓰지만, 요청 함수가 다른 스킴을 지원하면 공격자가
file://
로 파일을 읽거나gopher://
로 내부 서비스에 접근할 수 있습니다. 과거 SSRF 공격의 상당수는 gopher로 memcached 등 내부 서비스에 접근한 사례입니다. 반드시 의도한 프로토콜만 허용해야 하며, 보통 웹앱이라면 HTTP/HTTPS만 허용해야 합니다.
요약하면, 온라인에 떠도는 “간단한” SSRF 패치들은 이런 예외 케이스를 전혀 고려하지 않습니다. 눈에 띄는 악성 URL만 막고, 창의적인 공격자에겐 여전히 뚫릴 수 있습니다. 개발자는 방어 로직을 설계할 때 공격자처럼 생각해야 합니다. 다음으로, Node.js 예제를 통해 더 견고한 SSRF 방어를 단계별로 구현하는 방법을 살펴보겠습니다.
견고한 SSRF 방어 구축하기 (Node.js 예제)
SSRF를 제대로 막으려면 단일 체크만으로는 부족합니다. 요청 URL을 여러 계층에서 검증하고, 안전한 라이브러리 기능을 적극 활용해야 실수를 줄일 수 있습니다. 위에서 살펴본 교훈을 바탕으로, Node.js(Express + Axios) 환경에서 강화된 SSRF 방어법을 단계별로 구현해봅니다.
아래는 특정 CDN 도메인에서만 파일 다운로드를 허용하는 안전한 엔드포인트 예시입니다. URL 파싱, 허용 리스트, 인터셉터, 엄격한 요청 옵션 등으로 SSRF를 방지합니다:
import axios from "axios";
import { URL } from "url";
import express from "express";
const R = express.Router();
const ALLOWED_HOSTS = ["cdn.example.com"]; // 허용된 호스트명
// 짧은 타임아웃으로 Axios 인스턴스 생성
const downloader = axios.create({ timeout: 10000 });
// URL 정책을 강제하는 요청 인터셉터 등록
downloader.interceptors.request.use(config => {
const u = new URL(config.url, config.baseURL);
// HTTPS 프로토콜 및 허용 호스트만 허용
if (u.protocol !== "https:" || !ALLOWED_HOSTS.includes(u.hostname)) {
return Promise.reject(new Error(`Blocked hostname: ${u.hostname}`));
}
return config;
});
R.get("/file-proxy/download", async (req, res) => {
const url = req.query.url;
const fname = req.query.file_name;
if (!url || !fname) {
return res.status(400).send("Missing parameters");
}
try {
const resp = await downloader.get(url, {
responseType: "stream",
maxRedirects: 0, // 리다이렉트 허용 안 함
//validateStatus: status => status < 400 // 3xx도 직접 처리하도록 설정 가능
});
// 다운로드 헤더 설정
res.setHeader("Content-Disposition", `attachment; filename=${encodeURIComponent(fname)}`);
res.setHeader("Content-Type", resp.headers["content-type"] || "application/octet-stream");
// 응답 스트림을 바로 클라이언트로 전달
resp.data.pipe(res);
} catch (e) {
console.error("Blocked or failed download:", e.message);
res.status(400).send("Invalid or forbidden URL");
}
});
이 구현이 앞서 소개한 우회 기법들을 어떻게 막는지 하나씩 살펴봅니다:
URL 파싱 및 호스트 허용 리스트: Node의 표준
URL
생성자를 사용해 요청 URL을 파싱합니다(new URL(config.url)
). 이를 통해 URL의 각 컴포넌트를 정확히 식별할 수 있습니다.u.protocol
과u.hostname
을 체크해 교묘한 포맷 변형도 방지합니다. 예를 들어,https://cdn.cloudfront.net@evil.com/...
의u.hostname
은evil.com
이므로 인터셉터가 차단합니다. 오직ALLOWED_HOSTS
에 명시된 호스트만 허용합니다. 공격자가 유사 도메인이나 서브도메인을 시도해도 모두 차단됩니다.HTTPS만 허용: 인터셉터에서
u.protocol !== "https:"
를 검사해 HTTPS가 아닌 경우(다운그레이드 공격, file:// 등) 즉시 차단합니다. 단, 이 자체만으로는 HTTPS→HTTP 리다이렉트 우회를 막을 수 없으므로, 아래처럼 리다이렉트도 차단해야 합니다.리다이렉트 완전 차단:
maxRedirects: 0
옵션으로 Axios가 어떤 리다이렉트도 따라가지 않게 만듭니다. 302 등 리다이렉트 응답이 오면 바로 반환하고, 직접 처리합니다. (코드에서 3xx를 에러로 간주하도록 추가 옵션도 가능) 이렇게 하면 허용 호스트가 리다이렉트로 악성 주소로 넘기려 해도, 서버가 따라가지 않고 400 에러로 차단합니다. 예를 들어cdn.example.com
이evil.com
으로 리다이렉트하면, 서버는 이를 따르지 않고 바로 차단합니다. 참고: 만약 제한된 횟수의 리다이렉트만 허용하고 싶다면, 각 홉마다 검증이 필요하므로 복잡해집니다. 사용자 입력 URL엔 리다이렉트 자체를 아예 금지하는 것이 가장 안전합니다.인터셉터를 통한 중앙 집중 정책 적용: Axios 요청 인터셉터를 활용해, 해당 인스턴스의 모든 요청에 정책이 일관되게 적용됩니다. 검증 로직을 여기저기 흩뿌리지 않아도 되고, 개발자가 매번 직접 검증 함수를 호출하지 않아도 됩니다. 코드 차원에서의 defense-in-depth(다계층 방어) 역할도 하며, 실수로 검증을 빼먹어도 인터셉터가 최종적으로 막아줍니다.
타임아웃 및 에러 처리: SSRF 우회와 직접적 관련은 없지만, 느리거나 응답 없는 엔드포인트로 인한 서비스 지연을 막으려면 적절한 타임아웃(여기선 10초)을 설정하는 것이 좋습니다. 또한, 에러가 발생하면(인터셉터/요청 모두) 400 응답과 함께 에러 로그를 남깁니다. 에러 메시지는 “Blocked hostname: evil.com”처럼, 차단 원인을 로깅해 공격 탐지에 활용할 수 있습니다(단, 너무 상세한 정보를 사용자에게 노출하진 않도록 주의).
컨텐츠 검증: 위 예제는 정상 응답 후, 파일을 다운로드(
attachment; filename=...
)로 바로 전달합니다. 허용된 소스의 컨텐츠를 신뢰하는 경우입니다. 만약 파일 유형, 크기 등 추가 검증이 필요하다면(예: PDF만 허용, 특정 용량 제한 등), 파이프 전 체크를 추가하면 됩니다. 여기서는 SSRF 방어에 초점을 맞췄습니다.
이렇게 구현하면, 앞서 우회에 성공했던 테스트 케이스도 모두 안전하게 통과합니다. startsWith
기반 필터는 @
와 오픈 리다이렉트에 뚫렸지만, 새 구현은 진짜 호스트만 체크하고 리다이렉트도 아예 차단하므로 우회가 불가능합니다. 실제 테스트 예시:
- 허용 도메인 정상 요청: 허용된 CDN으로의 정상 요청은 200 OK로 성공
@
리다이렉트 페이로드 차단:...cloudfront.net@302.r3dir.me/...
는 400 에러와 함께 로그에 남음(인터셉터가hostname = 302.r3dir.me
로 감지)- 외부 도메인 직접 접근 차단:
https://evil.com/...
은 허용 리스트 미포함으로 즉시 차단 - 허용 호스트의 리다이렉트 차단: CDN이 리다이렉트 응답(테스트에선 nock으로 302 시뮬레이션)해도 서버가 따라가지 않고 400 반환
이처럼 URL 파싱과 HTTP 클라이언트 레벨의 정책 강제로 SSRF 위험을 크게 줄일 수 있습니다. 하지만 이걸로 끝이 아닙니다. 진짜 안전한 SSRF 방어는 여러 계층의 체크가 결합되어야만 가능합니다.
다계층 방어: 추가적인 보안 대책
위에서 소개한 코드 레벨의 보호 조치만으로도 강력한 출발점이 될 수 있습니다. 하지만 숙련된 공격자는 특히 복잡한 실제 환경에서 여전히 빈틈을 노릴 수 있습니다. 다음은 여러분(그리고 보안팀)이 반드시 고려해야 할 추가적인 다계층(Defense-in-Depth) 보안 대책입니다:
DNS 해석 및 IP 대역 검증: 호스트네임 허용 리스트만으로는 충분하지 않을 수 있으므로, 해당 도메인이 실제로 어디로 해석되는지 한 번 더 확인하는 것이 좋습니다. 예를 들어, 허용된 호스트의 IP 주소가 예상한 대역(예: 공인 IP)에 속하는지 반드시 검증하세요. 클라우드 환경에서는 일부 호스트네임이 내부 IP로 해석될 수 있으니 특히 주의해야 합니다. 요청 전에 DNS 조회를 수행하고, 결과 IP가 사설/내부 주소(RFC1918 대역, localhost, link-local 등)가 아닌지 확인하세요. 이렇게 하면 허용 도메인이 탈취되거나 DNS rebinding으로 서버가 내부망을 공격하는 시나리오를 막을 수 있습니다. 대부분의 언어에는 IP가 사설 네트워크에 속하는지 검사하는 라이브러리(예: Python의
ipaddress
모듈, Golang의net
패키지 등)가 있습니다. Node.js에서는dns.promises.lookup
으로 IP를 얻어 검사할 수 있습니다. 조회와 실제 요청 사이에 레이스 컨디션이 발생하지 않도록 주의하며, 위 인터셉터 방식을 커스텀 DNS 리졸버와 결합해 IP 필터링을 강화할 수도 있습니다.네트워크 이그레스 필터링(CIDR 허용/차단 목록): SSRF 방어를 애플리케이션 코드에만 의존하지 마세요. 네트워크 레벨의 제어가 추가적인 안전망이 될 수 있습니다. 예를 들어, 서비스가 특정 외부 API나 CDN에만 접근해야 한다면, 방화벽 규칙이나 클라우드 보안 그룹을 설정해 그 외 모든 아웃바운드 요청을 차단하세요. 대부분의 클라우드 제공자나 컨테이너 오케스트레이션 플랫폼은 이그레스(외부로 나가는 트래픽) 제한 기능을 제공합니다. 웹 애플리케이션 방화벽(WAF)이나 API 게이트웨이에서도 내부 IP 대역으로의 트래픽을 차단하는 규칙을 추가해, 애플리케이션 로직을 넘어선 또 다른 방어 계층을 구축할 수 있습니다.
허용 포트 및 프로토콜 제한: 만약 서비스가 표준 HTTP/S 포트만 사용한다면, 허용 포트를 명확히 제한하는 것이 좋습니다. SSRF 공격자는 비HTTP 서비스(예:
http://target:22/
로 SSH 배너 확인, 또는 포트 스캐닝 등)를 노릴 수 있습니다. HTTP 클라이언트가 해당 프로토콜을 직접 처리하지 않더라도, 단순 연결만으로도 정보가 노출될 수 있습니다. 허용 리스트 정책에서 80/443 외의 포트를 차단하거나, 프레임워크 URL 파서에서u.port
값을 검사(예:https/http
일 때 443/80 또는 빈 값만 허용)하는 식으로 제한할 수 있습니다. 마찬가지로,http
와https
이외의 스킴(ftp:
,file:
등)은 절대 허용하지 않도록 하세요.로깅 및 알림: SSRF 시도와 관련된 로그는 반드시 고위험 이벤트로 취급해야 합니다. 위 코드에서는 다운로드가 차단될 때
console.error
로 로깅하지만, 실제 운영 환경에서는 이런 로그를 모니터링 시스템으로 전송하는 것이 좋습니다. 예를 들어 “Blocked hostname: 169.254.169.254”와 같은 메시지가 반복된다면, 누군가 SSRF 취약점을 탐색 중임을 나타내는 강력한 신호입니다. 이런 패턴에 대해 경고 알림을 설정하세요. 요청 URL(또는 최소한 도메인)이 포함된 상세 접근 로그는 사고 대응 시 공격자가 어떤 시도를 했는지 추적하는 데 큰 도움이 됩니다. SSRF 공격은 때로 “블라인드” 방식(공격자가 직접 응답을 받지 않음)일 수 있으므로, 공격자가 엔드포인트를 퍼징하고 부수 효과를 관찰할 수도 있습니다. 이럴 때는 꼼꼼한 로깅이 문제가 있다는 유일한 단서가 될 수 있습니다.보안 테스트 및 자동화: SSRF 필터를 우회하는 방법이 워낙 다양하므로, 지속적인 테스트가 필수입니다. SSRF 테스트 케이스를 QA나 CI/CD 파이프라인에 통합하세요. 예를 들어, 앞서 소개한 테스트 스위트는
@
페이로드와 오픈 리다이렉트 시나리오를 자동으로 시도해 서버가 400을 반환하는지 확인합니다. 보안 스캐닝 도구를 활용하거나, 검증 로직에 대한 단위 테스트를 직접 작성할 수도 있습니다. 정적 분석도 도움이 됩니다—예를 들어 Semgrep이나 CodeQL 쿼리로 위험한 패턴(검증 없는axios.get(userInput)
등)을 탐지할 수 있습니다. 조직 차원에서는 정기적인 모의 해킹(침투 테스트)도 병행해야 합니다. Burp Suite 같은 도구는 SSRF 페이로드 목록을 제공하며, 다양한 변형(인코딩된 IP 등)을 자동으로 시도할 수 있습니다. 여러분의 “안전하다”는 코드가 실제로 이런 공격에 견딜 수 있는지 꼭 검증하세요.의존성 및 보안 지식 최신화: SSRF 관련 생태계는 계속 진화하고 있습니다. 새로운 우회 기법(예: 언어별 URL 파싱 취약점, 클라우드 메타데이터 서비스의 특이 동작 등)이 지속적으로 발견됩니다. 보안 권고에 항상 귀 기울이세요—예를 들어, SSRF 방어를 위해 만들어진
nossrf
npm 패키지조차도 치명적 우회 취약점이 발견된 바 있습니다. URL 요청과 관련된 라이브러리나 패키지는 항상 최신 패치 버전을 사용하고, 보안 패치 내역(Changelog)을 꼼꼼히 확인하세요. 그리고 절대 HTTP 클라이언트의 인증서 검증 등 핵심 보호 기능을 비활성화하지 마세요—이렇게 하면 공격자가 “안전한” 요청을 가로채거나 가짜 인증서를 제시할 수 있습니다.
요약하면, 다계층 방어란 한 단계의 방어가 실패할 수 있음을 전제로 여러 겹의 방어책을 두는 것입니다. 단순 접두사 검사만으로는 우회가 가능했지만, 코드에서 호스트명·프로토콜·리다이렉트 제어를 결합하니 우회가 훨씬 어려워졌습니다. 여기에 네트워크 규칙과 지속적인 테스트까지 더하면 공격자가 뚫고 들어오기 훨씬 더 힘들어집니다. 공격자가 넘어야 할 장애물이 많아질수록, 포기하거나 실수로 탐지될 확률도 높아집니다.
결론: “만능 SSRF 패치”는 없다
은탄환(Silver Bullet)은 없습니다.
전설에도, 보안에도 없습니다. 보안은 한 줄짜리 공식이 아니라, 생각하고, 테스트하고, 뚫리고, 고치고, 반복하는 과정입니다.
SSRF 취약점은 소프트웨어 보안의 더 큰 교훈을 줍니다: 만능 패치에 속지 마세요. “이 코드 한 줄이면 SSRF가 해결된다”는 식의 조각 코드가 있다면 반드시 의심하세요. 실제로 널리 쓰이는 “안전한” 패치조차도(예: ChatGPT-Next-Web의 초기 수정, 단순 허용 리스트 등) @
우회나 리다이렉트 트릭에 뚫릴 수 있었습니다. 공격자는 항상 정해진 틀 밖에서 생각하므로, 우리의 방어도 그 이상이어야 합니다.
SSRF Defense Lab: 실전 테스트베드
실제로 내 SSRF 방어책이 통하는지 검증하고 싶으신가요? 오픈소스 SSRF Defense Lab 프로젝트를 활용해보세요:
- 🧪 SSRF 보안을 위한 최소·자급형 Node.js/Express 테스트베드
- 🔧 실제 우회 사례가 포함된 안전/취약 라우터 예제
- 🛠️ CDN/도메인 허용 리스트, 리다이렉트 우회 등 자동화된 테스트 유틸리티
- 📋 모든 테스트를 통과해야만 안전한 라우터로 간주
주요 특징:
- 재사용 가능한 SSRF 보안 테스트 유틸리티
- 안전/취약 라우터 코드와 테스트
- 흔한 우회 기법(
@
트릭, 오픈 리다이렉트, 비허용 도메인 등) 모두 커버 - 실행도 간단:
npm install && npm test
소스 및 문서: github.com/windshock/ssrf-defense-lab
개발자 및 보안 담당자를 위한 핵심 요약:
핵심 요약
- 사용자가 제어하는 URL은 항상 엄격히 검증·정제하세요. 문자열 비교 대신 검증된 라이브러리로 파싱하세요.
- 방어를 계층화하세요. 호스트 허용 리스트, 프로토콜 검사, DNS/IP 검증, 리다이렉트 엄격 처리 등을 조합하세요.
- 공격자처럼 테스트하세요. 실제 우회 페이로드와 자동화된 테스트로 본인 방어책을 스스로 뚫어보세요.
- 다계층(Defense-in-Depth) 전략을 채택하세요. 애플리케이션 로직과 네트워크 레이어 통제를 모두 활용해 위험을 줄이고, 악용 시도를 모니터링하세요.
- 새로운 우회 기법에 항상 대비하세요. 보안은 끝이 없습니다—계속 배우고, 업데이트하고, 적응하세요.
SSRF 방어는 단일 패치로 끝나는 문제가 아닙니다. 견고하고 계층적인 방어를 구축하고, 방심하지 않는 것이 핵심입니다. 주기적으로 리뷰·테스트·도전하며 솔루션을 점검하세요. 여러분이 하지 않으면, 누군가는 반드시 시도할 것입니다. 보안은 설계 단계부터, 그리고 매번의 경험을 통해 더욱 단단해집니다.