회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
Q&A
지식공유
공지사항
DX마켓
통계
로그인 회원가입
고객센터

DX Security — SSRF 보안 플러그인

D DX관리자
2026.09.03 01:14 4 0



DX Security — SSRF 방어 v1.0.0 안내서

DesignOneX CMS SSRF 기업 납품 수준 방어 플러그인
PHP 5.6+ · IIS / Apache / Nginx · 공유호스팅 완전 호환
코어(og_fetch.php / DxMarket.php) 수정 없음 — 훅으로만 동작



목차

  1. SSRF란 무엇인가
  2. DXCMS 코어의 현재 SSRF 방어 수준
  3. 코어가 막지 못하는 것 — 이 플러그인의 역할
  4. 설치 방법
  5. 방어층 상세 설명
    • 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 탐지
  6. SSRF Gateway 전체 흐름
  7. 보안 로그 읽는 법
  8. 보안 강점 요약 — 기업 납품 공격 테스트 결과
  9. 방어하지 못하는 영역
  10. 개발자 API 가이드
  11. 자주 묻는 질문


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

 

첨부파일 1개 회원전용

댓글0

로그인 후 댓글을 작성할 수 있습니다.
Plugin 49
45
전체 회원
1,375
전체 게시글
2,737
전체 댓글
38
오늘 방문
53,476
전체 방문
0
현재 접속
인기글 7일 이내
최신글
최신댓글
내 플레이리스트
플레이리스트가 비어있습니다
스튜디오 게시판에서
플레이리스트에 담기 버튼을
눌러보세요
목록
목록