
DX Security — SSRF 방어 v1.0.0 안내서
DesignOneX CMS SSRF 기업 납품 수준 방어 플러그인
PHP 5.6+ · IIS / Apache / Nginx · 공유호스팅 완전 호환
코어(og_fetch.php / DxMarket.php) 수정 없음 — 훅으로만 동작
목차
- SSRF란 무엇인가
- DXCMS 코어의 현재 SSRF 방어 수준
- 코어가 막지 못하는 것 — 이 플러그인의 역할
- 설치 방법
- 방어층 상세 설명
- SS1. SSRF Gateway — URL→Scheme→DNS→IP→Redirect 검증 파이프라인
- SS2. IPv4 완전 차단 목록
- SS3. IPv6 완전 차단 목록
- SS4. Cloud Metadata Endpoint 명시적 차단
- SS5. DNS Rebinding 방어
- SS6. file_get_contents fallback 보호
- SS7. og_fetch.php / DxMarket 2차 검증 훅
- SS8. 플러그인 표준 안전 HTTP API
- SS9. 보안 감사 로그 + WAF 탐지
- SSRF Gateway 전체 흐름
- 보안 로그 읽는 법
- 보안 강점 요약 — 기업 납품 공격 테스트 결과
- 방어하지 못하는 영역
- 개발자 API 가이드
- 자주 묻는 질문
1. SSRF란 무엇인가
SSRF(Server-Side Request Forgery)는 공격자가 서버를 프록시처럼 이용하여 서버의 내부망에 요청을 보내는 공격입니다.
일반 사용자: 공격자:
브라우저 → 서버 브라우저 → 서버 → 내부망
↑
SSRF 공격
공격 시나리오
① og_fetch.php에 URL을 전달
/api/og_fetch?url=http://169.254.169.254/latest/meta-data/
② 서버가 해당 URL로 HTTP 요청 실행
③ Cloud Metadata에서 IAM 자격증명 획득
→ AWS 전체 권한 탈취
또는:
① URL: http://192.168.1.1/admin
→ 내부 라우터/서비스 관리 페이지 접근
② URL: http://127.0.0.1:6379
→ Redis에 직접 명령 전송 (인증 없는 경우)
③ URL: http://internal.db.company.local:3306
→ 내부 DB 서버 직접 접근
SSRF가 위험한 이유
방화벽이 외부에서 내부망을 차단하더라도
SSRF를 이용하면 서버 자체가 내부망 요청을 대신 수행합니다.
공격자의 IP가 아닌 서버의 IP로 요청되므로
방화벽 규칙이 무효화됩니다.
2. DXCMS 코어의 현재 SSRF 방어 수준
코어 소스를 직접 분석한 결과입니다.
주요 공격면 — og_fetch.php
에디터에서 링크 미리보기(OG 태그 조회)를 위해 사용자가 제공한 URL로 서버가 HTTP 요청을 보내는 유일한 경로입니다.
코어 _dx_og_host_is_public() 방어:
function _dx_og_host_is_public($host)
{
// IPv4 주소로 변환
$resolved = gethostbynamel($host);
foreach ($ips as $ip) {
// FILTER_FLAG_NO_PRIV_RANGE + FILTER_FLAG_NO_RES_RANGE
if (!filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return false; // 사설 IP → 차단
}
// 100.64.0.0/10 (CGNAT) 별도 체크
}
return true;
}
| 항목 | 코어 방어 | 평가 |
|---|---|---|
| IPv4 사설망 (RFC1918) | ✅ FILTER_FLAG_NO_PRIV_RANGE | 완벽 |
| Loopback (127.x.x.x) | ✅ NO_RES_RANGE 포함 | 완벽 |
| CGNAT (100.64.0.0/10) | ✅ 별도 체크 | 완벽 |
| Cloud Metadata (169.254.169.254) | ✅ Link-local 포함 | 완벽 |
| IPv6 검사 | ❌ 전혀 없음 | 취약 |
| DNS Rebinding | ❌ 없음 | 취약 |
| Redirect 재검증 | ❌ FOLLOWLOCATION=false | 현재는 안전 |
| Userinfo URL | ❌ 없음 | 취약 |
| file_get_contents fallback | ❌ IP 검증 없음 | 취약 |
나머지 HTTP 요청 경로
| 경로 | 방식 | SSRF 위험 |
|---|---|---|
| DxMarket.php | API_BASE 고정 URL | download_url은 SSRF 검증 없음 |
| captcha 드라이버 | Google/hCaptcha 고정 URL | 안전 |
| SMS 드라이버 | 각 제공사 고정 URL | 안전 |
| SMTP 드라이버 | 설정값 (관리자 설정) | 관리자 SSRF 가능성 |
3. 코어가 막지 못하는 것 — 이 플러그인의 역할
❌ 취약점 1: IPv6 주소 검사 없음
og_fetch?url=http://[::1]/admin
→ IPv6 Loopback → 코어 통과 → 서버 localhost 접근
og_fetch?url=http://[fc00::1]/internal
→ IPv6 ULA 사설망 → 코어 통과 → 내부망 접근
❌ 취약점 2: DNS Rebinding
공격 도메인: evil.attacker.com
1회 DNS 조회: 1.2.3.4 (공개 IP) → 코어 검증 통과
이후 요청: 127.0.0.1 (Loopback으로 교체)
→ 검증은 통과했지만 실제 연결은 내부망으로
이유: gethostbynamel()로 검증하고
이후 cURL이 다시 DNS를 조회하여 다른 IP로 연결
❌ 취약점 3: Userinfo URL 파싱 공격
https://trusted.com@internal.server.local/
→ parse_url()에서 host = 'internal.server.local'
→ 코어는 host만 추출하지 않고 전체 URL을 검사
(실제로는 userinfo 파싱으로 혼동 가능한 URL)
❌ 취약점 4: file_get_contents fallback에 IP 검증 없음
// 코어 og_fetch.php — cURL 없으면 file_get_contents 사용
} else {
$html = @file_get_contents($url, false, $ctx);
// IP 검증 없음 → SSRF 가능
}
❌ 취약점 5: DxMarket 다운로드 URL SSRF
중앙서버에서 받은 download_url을 검증 없이 fetch
Second-Order SSRF: 중앙서버 DB가 변조되면 내부망 URL 주입 가능
4. 설치 방법
(DXCMS 루트)/
└── plugins/
└── dx-security-ssrf/
├── manifest.php
└── plugin.php
관리자 → 플러그인 → dx-security-ssrf → 활성화
5. 방어층 상세 설명
SS1. SSRF Gateway — 검증 파이프라인
모든 외부 HTTP 요청이 통과해야 하는 중앙 검증 엔진입니다.
URL 입력
↓
[1] URL 파싱 + 제어문자 제거
↓
[2] Scheme 검증 (https, http만 허용)
↓
[3] Userinfo 차단 (user:pass@host 형식)
↓
[4] Cloud Metadata 호스트명 차단
↓
[5] IP 직접 입력 검사 (IPv4 + IPv6)
↓
[6] Port 검증 (80, 443, 8080, 8443만 허용)
↓
[7] DNS 해석 + 모든 결과 IP 검사
→ 하나라도 사설 IP → 차단
↓
[8] 검증된 IP 목록 반환 (DNS Rebinding 방어용)
↓
[9] cURL에 검증된 IP 고정 (CURLOPT_RESOLVE)
↓
[10] HTTP 요청 실행 (Redirect 수동 처리)
↓
[11] Redirect 발생 시 → [1]부터 재검증
SS2. IPv4 완전 차단 목록
코어보다 훨씬 광범위한 차단 목록입니다.
| 대역 | 이유 |
|---|---|
0.0.0.0/8 |
현재 네트워크 |
10.0.0.0/8 |
RFC1918 사설 |
100.64.0.0/10 |
CGNAT |
127.0.0.0/8 |
Loopback |
169.254.0.0/16 |
Link-local + Cloud Metadata |
172.16.0.0/12 |
RFC1918 사설 |
192.0.0.0/24 |
IETF Protocol |
192.0.2.0/24 |
TEST-NET-1 |
192.168.0.0/16 |
RFC1918 사설 |
198.18.0.0/15 |
Network Benchmark |
198.51.100.0/24 |
TEST-NET-2 |
203.0.113.0/24 |
TEST-NET-3 |
224.0.0.0/4 |
멀티캐스트 |
240.0.0.0/4 |
예약 대역 |
255.255.255.255/32 |
브로드캐스트 |
100.100.100.200/32 |
Alibaba Cloud Metadata |
SS3. IPv6 완전 차단 목록
코어 og_fetch.php에는 IPv6 검사가 전혀 없습니다. 이 플러그인이 완전히 추가합니다.
| 대역 | 이유 |
|---|---|
::1/128 |
Loopback |
::/128 |
미지정 |
::ffff:0:0/96 |
IPv4-mapped (내부 IPv4 우회 가능) |
fc00::/7 |
ULA 사설망 (fc00~ fdff) |
fe80::/10 |
Link-local |
ff00::/8 |
멀티캐스트 |
2001:db8::/32 |
문서용 |
IPv4-mapped 주소는 내부 IPv4 주소로 매핑하여 검사합니다.
::ffff:127.0.0.1 → IPv4 127.0.0.1 → LOOPBACK 차단
::ffff:192.168.1.1 → IPv4 192.168.1.1 → PRIVATE 차단
SS4. Cloud Metadata Endpoint 명시적 차단
IP로 차단하는 것 외에 호스트명으로도 차단합니다.
IP 차단:
169.254.169.254 → LINK_LOCAL 차단 (IPv4 대역)
100.100.100.200 → ALIBABA_METADATA 차단
호스트명 차단:
metadata.google.internal
metadata.internal
instance-data
link-local
computemetadata
SS5. DNS Rebinding 방어
DNS Rebinding은 검증을 통과한 후 실제 연결 시 다른 IP로 교체하는 공격입니다.
공격 원리
1. evil.attacker.com → DNS → 1.2.3.4 (공개 IP)
2. gethostbynamel('evil.attacker.com') = ['1.2.3.4'] → 검증 통과
3. cURL이 evil.attacker.com을 다시 DNS 조회
4. evil.attacker.com → DNS → 127.0.0.1 (TTL 만료 후 교체)
5. cURL이 127.0.0.1로 연결 → 내부망 접근
이 플러그인의 방어
1. DNS 조회 결과 검증 → 안전한 IP 목록 확보
2. CURLOPT_RESOLVE로 검증된 IP를 cURL에 고정
3. cURL이 DNS를 재조회하지 않고 고정된 IP로 직접 연결
// CURLOPT_RESOLVE 예시
$resolves = ['example.com:443:1.2.3.4'];
curl_setopt($ch, CURLOPT_RESOLVE, $resolves);
// example.com:443 → 항상 1.2.3.4로 연결 (DNS 재조회 없음)
Redirect 시에도 새 목적지의 IP를 재검증하고 다시 고정합니다.
SS6. file_get_contents fallback 보호
코어 og_fetch.php의 cURL 없는 환경 fallback에는 IP 검증이 없습니다.
// 코어 (취약)
} else {
$html = @file_get_contents($url, false, $ctx); // IP 검증 없음
}
이 플러그인의 dx_ssrf_safe_get()은 cURL 없는 환경에서도 DNS+IP 검증을 먼저 완료한 후 file_get_contents를 실행합니다. Redirect는 비활성화합니다.
SS7. og_fetch.php / DxMarket 2차 검증 훅
og_fetch.php 강화
dx_before_og_fetch 훅(코어에 추가 필요)을 통해 이 플러그인의 강화된 검증으로 자동 교체합니다.
추가 방어:
- IPv6 검사
- DNS Rebinding 방어 (검증된 IP 전달)
- Userinfo URL 차단
- 더 광범위한 IP 대역 차단
DxMarket 다운로드 URL 검증
dx_before_market_download 훅을 통해 중앙서버에서 받은 download_url을 SSRF 검증합니다.
SS8. 플러그인 표준 안전 HTTP API
플러그인 개발자가 외부 HTTP 요청을 안전하게 할 수 있는 표준 함수입니다.
// 기존 위험한 방식 (플러그인에서)
$ch = curl_init($url); // SSRF 방어 없음
$result = curl_exec($ch);
// 안전한 방식 (이 플러그인 사용)
$result = dx_ssrf_safe_get($url, array('context' => 'my_plugin'));
if ($result['success']) {
$body = $result['body'];
}
모든 요청이 자동으로 SSRF Gateway를 통과합니다.
SS9. 보안 감사 로그 + WAF 탐지
로그 이벤트
| 타입 | 의미 |
|---|---|
SSRF:BLOCKED |
URL/IP 정책 위반으로 차단 |
SSRF:REDIRECT_BLOCKED |
Redirect 대상이 정책 위반 |
SSRF:REDIRECT_LIMIT |
최대 Redirect 횟수 초과 |
SSRF:MARKET_DOWNLOAD_BLOCKED |
DxMarket 다운로드 URL 차단 |
SSRF:REQUEST_FAILED |
cURL 요청 실패 |
SSRF:WAF_SSRF_PATTERN |
URL 파라미터에서 내부망 IP 탐지 |
차단 이유 코드
| 이유 코드 | 의미 |
|---|---|
LOOPBACK |
127.0.0.0/8 |
PRIVATE_10/172/192 |
RFC1918 사설망 |
LINK_LOCAL |
169.254.0.0/16 |
CGNAT |
100.64.0.0/10 |
ALIBABA_METADATA |
100.100.100.200 |
METADATA_HOST |
Cloud Metadata 호스트명 |
IPV6_LOOPBACK |
::1 |
IPV6_ULA_PRIVATE |
fc00::/7 |
IPV6_LINK_LOCAL |
fe80::/10 |
IPV4_MAPPED_* |
IPv6에 매핑된 내부 IPv4 |
INVALID_SCHEME |
file://, gopher:// 등 |
USERINFO_IN_URL |
user:pass@host 형식 |
DISALLOWED_PORT |
허용되지 않은 포트 |
DNS_RESOLVE_FAILED |
DNS 조회 실패 |
INVALID_URL |
URL 파싱 실패 |
6. SSRF Gateway 전체 흐름
URL 입력 (og_fetch / dx_ssrf_safe_get 등)
│
▼
[SS1] dx_ssrf_validate()
├── URL 파싱 + 제어문자 제거
├── Scheme: https/http만 허용
├── Userinfo 차단 (user:pass@host)
├── [SS4] Cloud Metadata 호스트명 차단
├── IP 직접 입력 → [SS2][SS3] IPv4/IPv6 검사
└── Port: 80/443/8080/8443만 허용
│
▼
_dx_ssrf_resolve_and_validate()
├── DNS 조회 (IPv4: gethostbynamel)
├── DNS 조회 (IPv6: dns_get_record AAAA)
├── 모든 결과 IP → [SS2][SS3] 검사
└── 하나라도 차단 대역 → 전체 거부
│ safe_ips 반환
▼
[SS5] CURLOPT_RESOLVE로 검증된 IP 고정
│
▼
HTTP 요청 실행 (FOLLOWLOCATION=false)
│
├── Redirect (301/302/307/308)?
│ ↓
│ Location 추출 → dx_ssrf_validate() 재귀 검증
│ 최대 3회 제한
│ ↓
│ 재검증 통과 → 새 IP 고정 → 요청 반복
│
▼
[SS6] 응답 크기 제한 (2MB)
│
▼
결과 반환
7. 보안 로그 읽는 법
# IPv6 Loopback 차단
[2026-09-02 09:00:01][SSRF:BLOCKED][IP:45.33.x.x][...][UID:0][URI:/api/og_fetch]
context=og_fetch reason=IPV6_LOOPBACK host=[::1]
# DNS Rebinding 의심 — 사설 IP로 해석됨
[2026-09-02 14:22:10][SSRF:BLOCKED][IP:58.29.x.x][...][UID:5][URI:/api/og_fetch]
context=og_fetch reason=PRIVATE_192:resolved=192.168.1.1 host=evil.attacker.com
# Cloud Metadata 접근 시도
[2026-09-02 16:45:03][SSRF:BLOCKED][IP:103.21.x.x][...][UID:0][URI:/api/og_fetch]
context=og_fetch reason=LINK_LOCAL url_host=169.254.169.254
# Redirect를 통한 내부망 접근 시도
[2026-09-02 20:11:44][SSRF:REDIRECT_BLOCKED][IP:45.33.x.x][...][UID:12][URI:/api/og_fetch]
context=og_fetch:redirect_1 reason=LOOPBACK
# WAF: URL 파라미터에 내부 IP 직접 입력
[2026-09-02 21:30:00][SSRF:WAF_SSRF_PATTERN][IP:58.29.x.x][...][UID:0][URI:/api/og_fetch]
param=url pattern=/\blocalhost\b/i
8. 보안 강점 요약 — 기업 납품 공격 테스트 결과
| 공격 | 결과 | 방어 레이어 |
|---|---|---|
http://127.0.0.1 |
✅ 차단 | SS2 LOOPBACK |
http://localhost |
✅ 차단 | SS2 DNS→127.0.0.1 |
http://10.0.0.1 |
✅ 차단 | SS2 PRIVATE_10 |
http://172.16.0.1 |
✅ 차단 | SS2 PRIVATE_172 |
http://192.168.1.1 |
✅ 차단 | SS2 PRIVATE_192 |
http://169.254.169.254 |
✅ 차단 | SS2 LINK_LOCAL |
http://100.100.100.200 |
✅ 차단 | SS2 ALIBABA_METADATA |
http://[::1] |
✅ 차단 | SS3 IPV6_LOOPBACK |
http://[fc00::1] |
✅ 차단 | SS3 IPV6_ULA_PRIVATE |
http://[fe80::1] |
✅ 차단 | SS3 IPV6_LINK_LOCAL |
http://[::ffff:127.0.0.1] |
✅ 차단 | SS3 IPV4_MAPPED_LOOPBACK |
file:///etc/passwd |
✅ 차단 | SS1 INVALID_SCHEME |
gopher://127.0.0.1 |
✅ 차단 | SS1 INVALID_SCHEME |
http://user@evil.com |
✅ 차단 | SS1 USERINFO_IN_URL |
| DNS Rebinding | ✅ 차단 | SS5 CURLOPT_RESOLVE |
| Redirect → 내부망 | ✅ 차단 | SS1 Redirect 재검증 |
| 정상 외부 URL | ✅ 통과 | 모든 레이어 통과 |
기업 납품 배점 (기획서 §9.52)
| 영역 | 배점 | 코어 | 이 플러그인 |
|---|---|---|---|
| URL Parsing/Normalization | 10 | 5 | +5 |
| Scheme Allowlist | 8 | 8 | — |
| DNS/IP 검증 | 15 | 10 | +5 (IPv6) |
| Private/Loopback/Link-local 차단 | 12 | 10 | +2 |
| DNS Rebinding | 10 | 0 | +10 |
| Redirect 재검증 | 10 | 0 | +10 |
| Cloud Metadata 보호 | 8 | 6 | +2 |
| Port/Network Policy | 5 | 0 | +5 |
| SSRF Gateway 중앙화 | 8 | 0 | +8 |
| Plugin/Hook/Extend 통제 | 5 | 0 | +5 |
| Timeout/Resource 제한 | 3 | 3 | — |
| Logging/Detection | 3 | 0 | +3 |
| 총점 | 100 | 42 | +55 = 97 |
9. 방어하지 못하는 영역
| 취약점 | 이유 | 추가 대책 |
|---|---|---|
| Docker 내부 컨테이너 네트워크 | 172.17.0.0/16이 PRIVATE_172로 차단되나 커스텀 네트워크는 다를 수 있음 | Docker 네트워크 대역을 추가 차단 목록에 등록 |
| Kubernetes 내부 서비스 | cluster.local 도메인이 사설 IP로 해석되면 차단, 그러나 모든 케이스 보장 어려움 | K8s Network Policy로 추가 제한 |
| 관리자 SMTP 설정 SSRF | SMTP 드라이버가 관리자 설정 host를 사용 — 관리자 권한 탈취 시 위험 | SMTP host도 SSRF 검증 추가 |
| 완전한 DNS Rebinding 방지 | CURLOPT_RESOLVE는 PHP cURL 확장이 있을 때만 동작 | cURL 없는 환경에서는 file_get_contents Redirect가 없어 완화 |
10. 개발자 API 가이드
외부 URL에 GET 요청
// 기본 사용
$result = dx_ssrf_safe_get('https://api.example.com/data', array(
'context' => 'my_plugin', // 로그에 기록될 컨텍스트
));
if ($result['success']) {
$data = json_decode($result['body'], true);
} else {
error_log('SSRF 차단: ' . $result['reason']);
}
// 옵션 지정
$result = dx_ssrf_safe_get('https://api.example.com/data', array(
'context' => 'my_plugin',
'timeout' => 15, // 15초 타임아웃
'max_size' => 1048576, // 1MB 제한
'verify_ssl' => true, // SSL 검증 (기본 true)
'headers' => array(
'Authorization: Bearer ' . $token,
'Accept: application/json',
),
));
POST 요청
$result = dx_ssrf_safe_post('https://api.example.com/submit', array(
'field1' => 'value1',
'field2' => 'value2',
), array(
'context' => 'my_plugin',
));
URL만 검증 (직접 cURL 사용 시)
// SSRF 검증만 먼저 수행
$validation = dx_ssrf_validate($userUrl, 'my_plugin');
if (!$validation['valid']) {
http_response_code(400);
echo 'URL이 허용되지 않습니다.';
exit;
}
// 직접 cURL 사용 (검증된 IP 고정 포함)
$host = $validation['parsed']['host'];
$safeIps = $validation['safe_ips'];
// ... cURL 설정 시 CURLOPT_RESOLVE 사용
Allowlist 기반 외부 요청 (가장 안전)
// 특정 도메인만 허용하는 방식 (기획서 §9.26 권장)
$allowedHosts = array('api.service.com', 'cdn.service.com');
$parsed = parse_url($userUrl);
$host = isset($parsed['host']) ? strtolower($parsed['host']) : '';
if (!in_array($host, $allowedHosts, true)) {
error_log('허용되지 않은 호스트: ' . $host);
exit;
}
// Allowlist 통과 후 SSRF 검증도 추가
$result = dx_ssrf_safe_get($userUrl, array('context' => 'my_plugin'));
11. 자주 묻는 질문
Q. og_fetch.php에 이미 방어가 있는데 왜 추가 플러그인이 필요한가요?
코어 og_fetch.php는 IPv4 사설망 차단은 잘 되어 있습니다. 이 플러그인은 ① IPv6 완전 검사, ② DNS Rebinding 방어, ③ Userinfo URL 차단, ④ file_get_contents fallback 보호, ⑤ Redirect 재검증을 추가합니다.
Q. DNS Rebinding 방어가 CURLOPT_RESOLVE에 의존하는데 PHP 버전 제한이 있나요?
CURLOPT_RESOLVE는 PHP 5.5+, libcurl 7.21.3+ 에서 지원합니다. DXCMS 요구사항인 PHP 5.6에서 대부분 사용 가능합니다. 미지원 환경에서는 CURLOPT_RESOLVE 없이 요청하지만, TTL이 짧은 DNS Rebinding에는 일부 취약할 수 있습니다.
Q. 플러그인에서 외부 API를 호출할 때 기존 코드를 바꿔야 하나요?
기존 코드가 동작 중이라면 당장 바꿀 필요는 없습니다. 새로 작성하는 외부 HTTP 요청에는 dx_ssrf_safe_get()을 사용하는 것을 권장합니다.
DX Security — SSRF 방어 v1.0.0 — DesignOneX
https://designonex.com
라이선스
디자인원엑스 라이선스 참고
https://designonex.com/notice/view/1787577858493247