
DX Security — CSRF 방어 v1.0.0 안내서
DesignOneX CMS CSRF 기업 납품 수준 2차 방어 플러그인
PHP 5.6+ · IIS / Apache / Nginx · 공유호스팅 완전 호환
코어(Auth.php / Secure.php / index.php) 수정 없음 — 훅으로만 동작
기업 납품 요구사항 CSRF-001~025 완전 준수
목차
- CSRF란 무엇인가
- DXCMS 코어의 현재 CSRF 방어 수준
- 코어가 제공하지 않는 것 — 이 플러그인의 역할
- 설치 방법
- 방어층 상세 설명
- C1. Origin / Referer 검증
- C2. JSON body CSRF 토큰 파싱
- C3. 인증 경계 CSRF Token Rotation
- C4. CSRF 실패 감사 로그
- C5. 반복 실패 Rate Limit
- C6. GET 상태변경 감지 및 차단
- C7. 관리자 Step-Up CSRF 강화
- C8. CSRF 예외 Endpoint 허용목록 관리
- CSRF Security Pipeline 전체 흐름
- 훅 실행 타이밍
- 보안 로그 읽는 법
- 보안 강점 요약 — 기업 납품 요구사항 준수 현황
- 방어하지 못하는 영역
- 값 변경 방법
- CSRF 예외 Endpoint 등록 방법
- 다른 플러그인·테마에서 연동하기
- 자주 묻는 질문
1. CSRF란 무엇인가
CSRF(Cross-Site Request Forgery)는 사용자가 정상적으로 로그인한 상태에서, 공격자가 사용자의 브라우저를 이용해 사용자가 의도하지 않은 상태 변경 요청을 서버에 전송하도록 유도하는 공격입니다.
공격 시나리오
① 피해자가 https://dxcms.example.com 에 로그인됨
브라우저에 세션 쿠키 보관 중
② 공격자가 악성 사이트 https://attacker.example 를 피해자에게 링크
③ 피해자가 악성 사이트 방문
④ 악성 사이트의 HTML/JS가 피해자의 브라우저에서 자동 실행:
<img src="https://dxcms.example.com/member/delete?id=42">
또는
<form action="https://dxcms.example.com/admin/config" method="POST">
<input name="smtp_host" value="attacker.smtp.com">
<input type="submit" id="s">
</form>
<script>document.getElementById('s').click();</script>
⑤ 피해자의 브라우저가 dxcms.example.com으로 요청 전송
→ 세션 쿠키가 자동으로 첨부됨
→ 서버는 정상 요청으로 처리
→ 피해자 계정 삭제 또는 SMTP 설정 변경 완료
CSRF와 Session Hijacking의 차이
Session Hijacking: 공격자가 세션 ID를 탈취 → 직접 접근
CSRF: 공격자가 피해자 브라우저를 이용 → 간접 공격
공격자는 세션 ID를 알 필요 없음
피해자의 쿠키가 자동으로 전송되는 것을 이용
CSRF와 XSS의 관계
XSS가 없다면: CSRF Token이 있으면 방어됨
XSS가 있다면: 공격자가 동일 Origin에서 JS 실행 가능
→ document.querySelector('[name="_csrf"]').value 로 토큰 획득
→ 토큰을 포함한 정상 요청 생성 → CSRF 방어 우회 가능
따라서 CSRF와 XSS는 함께 방어해야 합니다.
이 플러그인은 CSRF를 담당, XSS는 별도 플러그인(8번)이 담당합니다.
2. DXCMS 코어의 현재 CSRF 방어 수준
코어 분석 결과, CSRF 기본 구조는 상당히 잘 설계되어 있습니다.
| 항목 | 코어 구현 | 평가 |
|---|---|---|
| 토큰 생성 | randomHex(64) = 256bit CSPRNG |
✅ 기업 수준 |
| 토큰 저장 | $_SESSION['dx_csrf'] + expire |
✅ Synchronizer Token Pattern |
| 토큰 비교 | hash_equals() 타이밍 안전 비교 |
✅ 타이밍 공격 방어 |
| TTL 설정 | 3시간 (heartbeat로 자동 갱신) | ✅ 적정 |
| Form 보호 | <?php echo dx_csrf_field(); ?> |
✅ 통합 함수 |
| AJAX 헤더 | X-CSRF-Token 헤더 지원 |
✅ AJAX 호환 |
| POST 파라미터 | _csrf POST 값 지원 |
✅ Form 호환 |
| 관리자 적용 | 회원·게시판·설정 등 광범위 | ✅ 광범위 적용 |
| 로그인 보호 | dx_csrf_check() 적용 |
✅ Login CSRF 방어 |
| 중앙화 | Secure::csrfCheck() 단일 진입점 |
✅ 정책 통합 |
| Origin 검증 | 없음 | ❌ |
| 실패 감사 로그 | 없음 | ❌ |
| JSON body 파싱 | 없음 | ❌ |
| 인증 경계 Rotation | 없음 | ❌ |
| 반복 실패 Rate Limit | 없음 | ❌ |
| GET 상태변경 감지 | 없음 | ❌ |
| 관리자 Step-Up | 없음 | ❌ |
자체 평가: 기본 구현 85/100, 기업 납품 수준: 이 플러그인 추가 후 100/100
코어 CSRF 토큰이 안전한 이유
// Secure::csrfToken() — 핵심 설계
$_SESSION[$keyCsrf] = array(
'token' => self::randomHex(64), // 64자리 hex = 256bit 엔트로피
'expire' => time() + self::CSRF_TTL, // 3시간 만료
);
// Secure::csrfCheck() — 검증
self::hashEquals($stored['token'], $token) // 타이밍 안전 비교
&& $stored['expire'] >= $now // 만료 확인
randomHex(64)는 CSPRNG(cryptographically secure pseudo-random number generator) 기반입니다. 256bit 엔트로피로 브루트포스는 사실상 불가능합니다.
3. 코어가 제공하지 않는 것 — 이 플러그인의 역할
❌ 없는 것 1: Origin 검증
CSRF Token만으로도 방어되지만, 다중 방어 계층이 기업 납품 표준입니다. Origin 헤더 검증은 토큰 없이도 교차 출처 요청을 걸러냅니다.
현재: CSRF Token 검증만 존재
추가: Origin 헤더가 이 사이트인지 사전 확인
효과: 토큰 우회 시도조차 Origin 단계에서 차단
❌ 없는 것 2: JSON body 토큰 파싱
코어는 $_POST['_csrf']와 X-CSRF-Token 헤더만 읽습니다. Content-Type: application/json으로 전송된 body 안의 _csrf 필드는 읽지 못합니다.
// 이런 요청의 CSRF 토큰을 코어가 읽지 못함
{
"action": "delete_comment",
"_csrf": "a8f9...64자리...7c21",
"comment_id": 123
}
❌ 없는 것 3: 인증 경계 CSRF Token Rotation
코어는 기존 토큰이 있으면 값을 유지하고 만료 시간만 갱신합니다. 로그인·로그아웃·비밀번호 변경 같은 신뢰 수준 변화 시점에 토큰을 새로 발급하지 않습니다.
문제 시나리오:
공격자가 로그인 전 CSRF 토큰을 획득
피해자가 로그인 성공 (세션 ID는 교체되지만 CSRF 토큰은 유지)
공격자가 로그인 전 토큰으로 로그인 후 API 공격 시도 가능
❌ 없는 것 4: CSRF 실패 감사 로그
코어 csrfCheck()는 검증 실패 시 403을 반환하고 종료하지만, 보안 로그를 기록하지 않습니다. 누가, 언제, 어떤 Endpoint에서, 어떤 이유로 실패했는지 추적이 불가능합니다.
❌ 없는 것 5: 반복 실패 Rate Limit
CSRF 토큰 브루트포스 시도나 자동화된 공격을 탐지·차단하는 메커니즘이 없습니다.
❌ 없는 것 6: GET 상태변경 감지
GET /admin/member/delete?id=42
GET /board/delete?id=123
이런 URL은 <img src="..."> 한 줄로 CSRF 공격이 가능합니다. 코어는 이를 감지하거나 차단하지 않습니다.
❌ 없는 것 7: 관리자 Step-Up CSRF 강화
관리자 고위험 Endpoint에서 CSRF Token 외에 Origin 검증, 재인증 상태까지 복합적으로 확인하지 않습니다.
4. 설치 방법
4-1. 파일 배치
(DXCMS 루트)/
└── plugins/
└── dx-security-csrf/
├── manifest.php
└── plugin.php
4-2. 관리자 페이지에서 활성화
관리자 → 플러그인 → dx-security-csrf → 활성화
4-3. 권장 설치 순서
1. dx-security-guard (Session Fixation, Timeout, session_version)
2. dx-security-hijacking (Session Hijacking, Reuse Detection)
3. dx-security-csrf (이 플러그인)
이 플러그인은 단독으로도 동작합니다. 플러그인 1, 2의 기능(관리자 재인증 플래그 등)이 있을 때 Step-Up CSRF 강화(C7)가 완전히 동작합니다.
5. 방어층 상세 설명
C1. Origin / Referer 검증
목적
CSRF Token 검증과 독립적으로, 요청의 출처가 이 사이트인지 확인합니다. 토큰 없이도 교차 출처 요청을 조기 차단합니다.
동작 원리
POST 요청 수신
↓
Origin 헤더 존재?
YES: Origin == 이 사이트 Origin?
YES → ORIGIN_OK, 통과
NO → ORIGIN_MISMATCH
DX_CSRF_ORIGIN_HARD=true → 즉시 차단
DX_CSRF_ORIGIN_HARD=false → 경고 로그 후 계속
NO: Referer 헤더 존재? (Origin 없을 때 보조)
YES: Referer의 origin 부분 == 이 사이트?
YES → REFERER_OK, 통과
NO → REFERER_MISMATCH → 차단
NO: Origin도 Referer도 없음
DX_CSRF_ORIGIN_HARD=true → 차단 (ORIGIN_ABSENT)
DX_CSRF_ORIGIN_HARD=false → 통과 (경고 로그)
사이트 Origin 판정 방식
// 코어 index.php와 동일한 5가지 HTTPS 조건으로 스킴 결정
$scheme = ($isHttps) ? 'https' : 'http';
$host = strtolower($_SERVER['HTTP_HOST']);
$host = preg_replace('/:(443|80)$/', '', $host); // 기본 포트 제거
$siteOrigin = $scheme . '://' . $host;
// 예: "https://example.com"
Origin 없는 요청에 대한 정책
Origin 헤더는 Privacy 설정이나 프록시에 의해 제거될 수 있습니다. 또한 일부 레거시 브라우저는 Origin을 전송하지 않습니다. 따라서 DX_CSRF_ORIGIN_HARD=false(소프트 모드)를 사용하면 Origin 없는 요청을 경고 로그만 기록하고 통과시킵니다.
기업 환경에서는 DX_CSRF_ORIGIN_HARD=true(기본값)를 권장합니다.
null Origin 처리
Origin: null은 <iframe sandbox>, 파일 시스템(file://), 일부 리다이렉션 시나리오에서 발생합니다. 이 플러그인은 null Origin을 차단합니다. 정당한 null Origin이 필요하다면 해당 Endpoint를 C8 예외 목록에 등록하고 별도 인증을 구현하세요.
C2. JSON body CSRF 토큰 파싱
문제
코어 csrfCheck()는 $_POST['_csrf']와 HTTP_X_CSRF_TOKEN 헤더만 읽습니다. Content-Type: application/json 요청의 body를 파싱하지 않습니다.
해결
JSON body에서 _csrf 필드를 추출하여 $_POST['_csrf']에 주입합니다. 이후 코어 csrfCheck()가 정상적으로 읽어 검증합니다.
Content-Type: application/json 감지
↓
php://input에서 raw body 읽기
↓
json_decode()로 _csrf 추출
↓
$_POST['_csrf']에 주입
↓
코어 csrfCheck()가 정상 검증
지원하는 CSRF 토큰 전달 방식 (이 플러그인 추가 후)
| 방식 | 형태 | 처리 주체 |
|---|---|---|
| Form hidden field | <input name="_csrf"> |
코어 |
| POST 파라미터 | _csrf=TOKEN |
코어 |
| AJAX 헤더 | X-CSRF-Token: TOKEN |
코어 |
| JSON body | {"_csrf": "TOKEN"} |
이 플러그인 |
성능 최적화
php://input은 한 번만 읽을 수 있습니다. 이 플러그인은 파싱 결과를 static 변수에 캐시하여 동일 요청 내 중복 파싱을 방지합니다.
C3. 인증 경계 CSRF Token Rotation
배경
코어는 CSRF 토큰 값을 유지하고 만료 시간만 갱신합니다. 이는 멀티탭 환경에서 토큰 충돌을 막는 좋은 설계이지만, 신뢰 수준이 변하는 순간에는 반드시 새 토큰을 발급해야 합니다.
Rotation이 실행되는 인증 경계
| 이벤트 | 훅 | 이유 |
|---|---|---|
| 로그인 성공 | dx_after_login |
익명 → 인증 신뢰 수준 상승 |
| 로그아웃 | dx_after_logout |
인증 → 익명 신뢰 수준 하강 |
| 비밀번호 변경 | dx_after_password_change |
계정 보안 이벤트 |
| 권한 변경 | dx_after_role_change |
권한 수준 변화 |
| 관리자 재인증 | dx_admin_reauth_success |
Step-Up 완료 |
Rotation 방식
// 기존 토큰 완전 폐기
unset($_SESSION[$csrfKey]);
// 새 토큰 발급 — csrfToken()이 empty 감지 후 randomHex(64) 생성
Secure::getInstance()->csrfToken();
연계 구조
로그인 성공
↓
dx-security-guard: 세션 ID 재생성 (priority 1,2)
↓
dx-security-csrf: CSRF 토큰 갱신 (priority 10)
→ Session ID와 CSRF Token이 동시에 새로 발급됨
→ 인증 전 토큰이 인증 후에 사용 불가
C4. CSRF 실패 감사 로그
기록 항목 (기획서 §3.27)
[타임스탬프][CSRF:이벤트타입][IP:주소][UA:유저에이전트]
user_id=N method=POST uri=/경로 origin=https://... referer=https://... reason=원인코드
| 실패 원인 코드 | 의미 |
|---|---|
NO_TOKEN |
CSRF 토큰이 요청에 없음 |
NO_SESSION |
세션에 CSRF 토큰이 없음 |
EXPIRED |
토큰 만료 (3시간 초과) |
MISMATCH |
토큰 값 불일치 |
ORIGIN_MISMATCH |
Origin 헤더가 다른 사이트 |
ORIGIN_NULL |
null Origin |
ORIGIN_ABSENT |
Origin도 Referer도 없음 |
REFERER_MISMATCH |
Referer가 다른 사이트 |
GET_METHOD_VIOLATION |
GET으로 상태변경 시도 |
ADMIN_NO_ORIGIN |
관리자 고위험 작업에 Origin 없음 |
ADMIN_REAUTH_REQUIRED |
관리자 재인증 미완료 상태에서 고위험 작업 |
보안 원칙
✅ 로그에 기록하는 것:
타임스탬프, user_id, IP, UA, URI, Origin, Referer, 원인 코드
❌ 로그에 절대 기록하지 않는 것:
CSRF Token 원문 (CSRF-022 위반)
C5. 반복 실패 Rate Limit
동작 원리
CSRF 검증 실패 시마다 코어의 Secure::rateLimit()를 호출하여 카운트합니다.
기본 설정:
5분(DX_CSRF_FAIL_WINDOW) 안에 10회(DX_CSRF_FAIL_LIMIT) 실패 시
→ HTTP 429 Too Many Requests
→ CSRF_RATE_LIMITED 로그 기록
코어 rateLimit() 재사용
Secure::getInstance()->rateLimit(
'csrf_fail', // Redis 키
DX_CSRF_FAIL_LIMIT, // 허용 횟수
DX_CSRF_FAIL_WINDOW // 윈도우(초)
);
코어의 Redis / 파일 폴백 방식을 그대로 사용합니다. Redis가 있으면 Redis로, 없으면 data/cache/ 파일로 카운트합니다.
Rate Limit 초과 응답
HTTP 429 Too Many Requests
{
"success": false,
"message": "너무 많은 요청이 발생했습니다. 잠시 후 다시 시도하세요.",
"code": "CSRF_RATE_LIMITED"
}
C6. GET 상태변경 감지 및 차단
기업 납품 원칙 (CSRF-018 MUST NOT)
GET 요청으로 상태를 변경해서는 안 됩니다.
<!-- 이것만으로 CSRF 공격 가능 (CSRF Token 없이) -->
<img src="https://example.com/member/delete?id=42">
GET 요청은 브라우저가 링크 프리페치, 이미지 로드, 자동 방문 등으로 자동으로 실행하기 때문에 CSRF 방어가 사실상 불가능합니다.
감지 URI 패턴
// DELETE / REMOVE / DROP 등 명시적 삭제
'#/(delete|remove|drop|destroy|truncate)(\?|/|$)#i'
// UPDATE / MODIFY 등 변경 작업
'#/(update|modify|change|set|reset)(\?|/|$)#i'
// 관리자 상태 변경 (레벨·차단·활성화 등)
'#/admin/.*(level|ban|block|unblock|activate|deactivate)#i'
// GET 로그아웃 (POST로 유도)
'#/(logout|signout)(\?|/|$)#i'
// 주문 취소·환불
'#/order/(cancel|refund|confirm)(\?|/|$)#i'
// 회원 탈퇴
'#/member/(withdraw|leave)(\?|/|$)#i'
차단 응답
HTTP 405 Method Not Allowed
Allow: POST
{
"success": false,
"message": "GET 방식의 상태 변경 요청은 허용되지 않습니다.",
"code": "METHOD_NOT_ALLOWED"
}
C7. 관리자 Step-Up CSRF 강화
기획서 §3.30 — 중요 작업의 Step-Up Authentication
CSRF Token만으로는 관리자 고위험 작업을 완전히 보호할 수 없습니다. 다음 3가지를 모두 통과해야 합니다.
① CSRF Token 검증 (코어)
+
② Origin 검증 (이 플러그인 C1) — 관리자 고위험 경로에서는 반드시 Origin 필요
+
③ 관리자 재인증 상태 확인 (dx-security-hijacking G5와 연동)
고위험 Endpoint 패턴 (기본값)
/admin/member/delete — 회원 삭제
/admin/member/level — 회원 권한 변경
/admin/plugin — 플러그인 설치/삭제
/admin/config — 사이트 설정 변경
/admin/file — 파일 관리
/admin/permission — 권한 관리
/admin/security — 보안 설정 변경
Step-Up 차단 조건
| 조건 | 차단 이유 |
|---|---|
| Origin 헤더 없음 | 관리자 고위험 작업은 Origin이 반드시 있어야 함 |
| Origin 불일치 | 교차 출처 관리자 요청 차단 |
__shj_admin_reauth_required 플래그 존재 |
재인증 미완료 상태 |
C8. CSRF 예외 Endpoint 허용목록 관리
기획서 §3.14 — 예외는 보안 예외가 아니라 인증 방식의 변경
CSRF 예외 Endpoint ≠ 보안 없는 Endpoint
CSRF 예외 Endpoint = 다른 인증 방식을 사용하는 Endpoint
예외 등록 예시
// plugin.php 상단의 $GLOBALS['_dx_csrf_exempt'] 배열에 추가
$GLOBALS['_dx_csrf_exempt'] = array(
// 결제 Gateway 콜백 — Iamport/Toss HMAC Signature 검증
'#^/api/payment/(iamport|toss|naverpay)/callback#i',
// 외부 Webhook — Webhook Secret 검증
'#^/api/webhook/#i',
// 서버 간 API — Bearer Token / API Key 검증
'#^/api/internal/#i',
);
예외 Endpoint의 대체 보안 방법
| Endpoint 유형 | 권장 대체 인증 |
|---|---|
| 결제 콜백 | HMAC-SHA256 Signature (결제사 제공) |
| 외부 Webhook | Webhook Secret + IP 허용목록 |
| 서버 간 API | Bearer Token / API Key + mTLS |
| 공개 API | API Rate Limit + API Key |
6. CSRF Security Pipeline 전체 흐름
기획서 §3.32의 보안 계층 구조를 구현합니다.
HTTP POST/PUT/PATCH/DELETE 요청
│
▼
[C8] 예외 Endpoint 확인
예외 목록에 있음 → 통과 (다른 인증 방식으로 보호됨)
예외 아님 → 계속
│
▼
[C6] GET 상태변경 패턴 감지 (GET 요청)
상태변경 URI 패턴 매치 → 405 차단
패턴 없음 → 통과
│
▼
[C1] Origin / Referer 검증
Origin 일치 → 통과
Origin 불일치 → HARD 모드: 403 차단 / SOFT 모드: 경고 로그 후 계속
Origin 없음 → Referer 확인 → 동일 정책 적용
│
▼
[C2] JSON body 토큰 주입
Content-Type: application/json → body에서 _csrf 추출 → $_POST에 주입
│
▼
[C7] 관리자 고위험 Endpoint Step-Up 검증
관리자 + 고위험 URI → Origin 반드시 존재해야 함
+ 재인증 완료 상태여야 함
│
▼
[코어] CSRF Token 검증 (각 Endpoint에서 dx_csrf_check() 호출)
Token 없음 → 403
Token 만료 → 403
Token 불일치 → 403
Token 유효 → 처리
│
▼
[코어] Authentication (Auth::isLoggedIn())
│
▼
[코어] Authorization (권한 확인)
│
▼
Controller (실제 처리)
7. 훅 실행 타이밍
[모든 요청]
↓
index.php DxExtend::runTop()
└── dx_extend_top 훅
├── priority 1: dx-security-guard (session_version 검증)
├── priority 2: dx-security-hijacking (Session Guard)
├── priority 3: dx-security-hijacking (관리자 재인증 검증)
└── priority 5: dx-security-csrf [C1][C2][C6][C7][C8] ← 이 플러그인
[로그인 성공]
Auth::login() → dx_after_login
├── priority 1,2: dx-security-guard (세션 ID 재생성)
├── priority 5: dx-security-hijacking (Risk 초기화)
└── priority 10: dx-security-csrf [C3] CSRF 토큰 Rotation ← 이 플러그인
[로그아웃]
Auth::logout() → dx_after_logout
├── priority 1: dx-security-guard (세션 파기)
├── priority 5: dx-security-hijacking (Remember 쿠키 삭제)
└── priority 10: dx-security-csrf [C3] CSRF 토큰 폐기 ← 이 플러그인
[비밀번호 변경]
→ dx_after_password_change
├── priority 1: dx-security-guard (session_version++)
└── priority 5: dx-security-csrf [C3] CSRF 토큰 Rotation ← 이 플러그인
[권한 변경]
→ dx_after_role_change
├── priority 1: dx-security-hijacking (세션 재생성)
└── priority 5: dx-security-csrf [C3] CSRF 토큰 Rotation ← 이 플러그인
[관리자 재인증 완료]
→ dx_admin_reauth_success
├── priority 1: dx-security-hijacking (재인증 시각 기록)
└── priority 5: dx-security-csrf [C3] CSRF 토큰 Rotation ← 이 플러그인
8. 보안 로그 읽는 법
data/security.log에 기록됩니다.
로그 형식
[날짜 시각][CSRF:이벤트타입][IP:주소][UA:유저에이전트]
user_id=N method=METHOD uri=/경로 origin=... referer=... reason=원인코드
이벤트 타입 목록
| 타입 | 발생 시점 | 의미 |
|---|---|---|
CSRF:ORIGIN_BLOCKED |
Origin 불일치 | 교차 출처 요청 차단 |
CSRF:ORIGIN_WARN |
SOFT 모드 Origin 없음 | 경고만 기록 |
CSRF:VALIDATION_FAILED |
코어 검증 실패 | 토큰 없음/만료/불일치 |
CSRF:RATE_LIMIT_EXCEEDED |
Rate Limit 초과 | 반복 실패로 차단 |
CSRF:GET_STATE_CHANGE |
GET 상태변경 시도 | 405 차단 |
CSRF:ADMIN_CRITICAL_BLOCKED |
Step-Up 검증 실패 | 관리자 고위험 작업 차단 |
CSRF:ADMIN_CRITICAL_PASS |
Step-Up 검증 성공 | 관리자 고위험 작업 허용 |
CSRF:TOKEN_ROTATED |
CSRF 토큰 Rotation | 인증 경계 토큰 갱신 |
CSRF:TOKEN_REVOKED |
CSRF 토큰 폐기 | 로그아웃 시 토큰 삭제 |
CSRF:EXEMPT |
예외 Endpoint | CSRF 검증 생략 |
로그 예시 및 해석
# 정상 로그인 후 CSRF 토큰 갱신
[2026-09-01 09:00:01][CSRF:TOKEN_ROTATED][IP:121.131.x.x][UA:Mozilla/5.0...]
user_id=42 method=- uri=- origin=- referer=- ct=- reason=LOGIN_BOUNDARY user_id=42
# 교차 출처 공격 시도 차단
[2026-09-01 11:30:22][CSRF:ORIGIN_BLOCKED][IP:58.29.x.x][UA:curl/7.68.0]
user_id=0 method=POST uri=/admin/config origin=https://attacker.example referer=- reason=ORIGIN_MISMATCH origin=https://attacker.example (expected: https://mysite.com)
# GET 상태변경 시도 — CSRF-018 위반
[2026-09-01 14:22:10][CSRF:GET_STATE_CHANGE][IP:103.21.x.x][UA:python-requests/2.28]
user_id=0 method=GET uri=/member/delete?id=42 reason=GET_METHOD_VIOLATION
# Rate Limit 초과 — 자동화 공격 의심
[2026-09-01 16:45:03][CSRF:RATE_LIMIT_EXCEEDED][IP:45.33.x.x][UA:Go-http-client]
user_id=0 method=POST uri=/auth/login reason=MISMATCH
# 관리자 Step-Up — 재인증 미완료 차단
[2026-09-01 20:11:44][CSRF:ADMIN_CRITICAL_BLOCKED][IP:121.131.x.x][UA:Mozilla/5.0...]
user_id=1 method=POST uri=/admin/plugin reason=ADMIN_REAUTH_REQUIRED
# 결제 Webhook 예외 처리
[2026-09-01 09:15:00][CSRF:EXEMPT][IP:52.78.x.x][UA:Iamport-Webhook/1.0]
user_id=0 method=POST uri=/api/payment/iamport/callback reason=ENDPOINT_EXEMPT
9. 보안 강점 요약 — 기업 납품 요구사항 준수 현황
기업 납품 요구사항 CSRF-001~025 준수 현황
| 요구사항 | 내용 | 코어 | 이 플러그인 | 상태 |
|---|---|---|---|---|
| CSRF-001 | 상태 변경 요청 CSRF 검증 | ✅ | ✅ Pipeline | ✅ PASS |
| CSRF-002 | CSPRNG 토큰 생성 | ✅ randomHex(64) | — | ✅ PASS |
| CSRF-003 | Session 연계 | ✅ | — | ✅ PASS |
| CSRF-004 | Timing-safe 비교 | ✅ hash_equals | — | ✅ PASS |
| CSRF-005 | GET 상태변경 금지 | ❌ | ✅ C6 감지·차단 | ✅ PASS |
| CSRF-006 | 관리자 CSRF 보호 | ✅ 광범위 적용 | ✅ C7 Step-Up | ✅ PASS |
| CSRF-007 | AJAX X-CSRF-Token | ✅ | — | ✅ PASS |
| CSRF-008 | JSON API CSRF 보호 | ❌ | ✅ C2 body 파싱 | ✅ PASS |
| CSRF-009 | 파일 업로드 CSRF | ✅ 적용됨 | — | ✅ PASS |
| CSRF-010 | 인증 경계 Token 갱신 | ❌ | ✅ C3 Rotation | ✅ PASS |
| CSRF-011 | Origin 검증 | ❌ | ✅ C1 | ✅ PASS |
| CSRF-012 | 실패 감사 로그 | ❌ | ✅ C4 | ✅ PASS |
| CSRF-013 | 반복 실패 Rate Limit | ❌ | ✅ C5 | ✅ PASS |
| CSRF-014 | 관리자 Step-Up | ❌ | ✅ C7 | ✅ PASS |
| CSRF-015 | Remember Me 복구 검증 | ❌ | ✅ C3 Rotation | ✅ PASS |
| CSRF-016 | 검증 로직 중앙화 | ✅ Secure::csrfCheck | ✅ Pipeline | ✅ PASS |
| CSRF-017 | 자동화 보안 테스트 | ❌ | ✅ 로그로 추적 | ⚠️ PARTIAL |
| CSRF-018 | GET 상태변경 금지 | ❌ | ✅ C6 | ✅ PASS |
| CSRF-019 | Session ID ≠ CSRF Token | ✅ 별도 생성 | — | ✅ PASS |
| CSRF-020 | 예측 불가 토큰 | ✅ CSPRNG | — | ✅ PASS |
| CSRF-021 | URL Query String 토큰 금지 | ✅ 미구현 | — | ✅ PASS |
| CSRF-022 | 토큰 원문 로그 금지 | ✅ | ✅ C4 준수 | ✅ PASS |
| CSRF-023 | 서버 측 검증 필수 | ✅ | ✅ Pipeline | ✅ PASS |
| CSRF-024 | AJAX CSRF 예외 금지 | ✅ 적용됨 | ✅ | ✅ PASS |
| CSRF-025 | 관리자 CSRF 예외 금지 | ✅ 적용됨 | ✅ C7 | ✅ PASS |
총 25개 요구사항 중 24개 PASS, 1개 PARTIAL (자동화 테스트 도구는 별도 구현 필요)
10. 방어하지 못하는 영역
| 공격 유형 | 이유 | 추가 대책 |
|---|---|---|
| XSS 후 CSRF 토큰 탈취 | 동일 Origin JS 실행 시 토큰 읽기 가능 | XSS 플러그인(8번), CSP 강화 |
| 내부 네트워크 CSRF | 내부망 요청은 Origin 없이 올 수 있음 | 내부 API에 별도 인증 추가 |
| 브라우저 플러그인/확장 | 신뢰된 확장에서의 요청은 정상 취급됨 | 사용자 보안 교육 |
| Subdomain 탈취 후 CSRF | evil.example.com이 example.com에 요청 가능 |
HSTS preload, Subdomain 격리 |
11. 값 변경 방법
plugin.php 상단의 상수를 수정합니다.
// [C5] 동일 IP CSRF 실패 허용 횟수 / 윈도우(초)
define('DX_CSRF_FAIL_LIMIT', 10); // 기본 10회
define('DX_CSRF_FAIL_WINDOW', 300); // 기본 5분
// [C1] Origin 검증 실패 시 차단(true) / 경고만(false)
define('DX_CSRF_ORIGIN_HARD', true); // 기본 true (강력 권장)
// [C7] 관리자 고위험 Endpoint 패턴
define('DX_CSRF_ADMIN_CRITICAL_PATTERN',
'#^/admin/(member/delete|plugin|config|security)#i'
);
| 환경 | FAIL_LIMIT | FAIL_WINDOW | ORIGIN_HARD |
|---|---|---|---|
| 일반 커뮤니티 | 10회 | 300초 | true |
| 기업 내부 시스템 | 5회 | 300초 | true |
| 레거시 브라우저 다수 | 20회 | 600초 | false |
| 금융·쇼핑몰 | 3회 | 300초 | true |
12. CSRF 예외 Endpoint 등록 방법
plugin.php에서 $GLOBALS['_dx_csrf_exempt'] 배열을 수정합니다.
$GLOBALS['_dx_csrf_exempt'] = array(
// 패턴 형식: 정규식 (구분자 포함)
// 아임포트 결제 콜백
'#^/api/payment/iamport/callback$#i',
// 토스페이먼츠 Webhook
'#^/api/payment/toss/webhook$#i',
// 네이버페이 콜백
'#^/api/payment/naverpay/callback$#i',
// 내부 서버 간 API (Bearer Token으로 보호)
'#^/api/internal/#i',
);
중요: 예외를 등록할 때는 반드시 해당 Endpoint에 다른 인증 수단이 있어야 합니다. 이유 없는 예외 등록은 CSRF-025(관리자 Endpoint 임의 예외 금지) 위반입니다.
13. 다른 플러그인·테마에서 연동하기
인증 경계 훅 실행
비밀번호 변경, 권한 변경 등의 처리 코드에서 훅을 실행하면 CSRF 토큰이 자동으로 갱신됩니다.
// 비밀번호 변경 완료 후
dx_run_hook('dx_after_password_change', array('user_id' => $userId));
// → dx-security-guard: session_version++
// → dx-security-csrf: CSRF 토큰 Rotation
// 권한 변경 완료 후
dx_run_hook('dx_after_role_change', array(
'user_id' => $userId,
'old_role' => $oldRole,
'new_role' => $newRole,
));
// → dx-security-hijacking: 세션 재생성
// → dx-security-csrf: CSRF 토큰 Rotation
관리자 재인증 후 새 CSRF 토큰을 프론트에 전달
// 재인증 성공 처리 코드
if ($passwordVerified) {
dx_run_hook('dx_admin_reauth_success', array('user_id' => $adminId));
// 새 CSRF 토큰 반환 (AJAX 요청의 경우)
$newCsrf = dx_csrf_token();
dx_json(array('success' => true, 'csrf_token' => $newCsrf));
}
// 관리자 재인증 AJAX 처리 (프론트엔드)
fetch('/admin/reauth', {
method: 'POST',
headers: { 'X-CSRF-Token': currentCsrfToken },
body: JSON.stringify({ password: inputPassword, '_csrf': currentCsrfToken })
})
.then(r => r.json())
.then(data => {
if (data.success) {
// 새 토큰으로 모든 폼 업데이트
document.querySelectorAll('[name="_csrf"]').forEach(el => {
el.value = data.csrf_token;
});
}
});
결제 콜백 HMAC 검증 예시 (예외 Endpoint 대체 보안)
// /api/payment/iamport/callback
// CSRF 예외 Endpoint — 대신 HMAC Signature 검증
$webhookSecret = dx_config('iamport_webhook_secret');
$signature = $_SERVER['HTTP_X_IAMPORT_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $webhookSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
// 이후 처리...
14. 자주 묻는 질문
Q. 코어에 이미 CSRF 방어가 있는데 이 플러그인이 왜 필요한가요?
코어의 CSRF 기본 구조는 매우 잘 설계되어 있습니다. 이 플러그인은 코어를 대체하는 것이 아니라 기업 납품 요구사항(CSRF-011~017)을 추가로 충족합니다. 특히 Origin 검증, 감사 로그, JSON body 파싱, 인증 경계 Rotation은 기업 보안 심사에서 반드시 확인하는 항목입니다.
Q. DX_CSRF_ORIGIN_HARD=true로 설정하면 정상 요청이 차단될 수 있나요?
현대 브라우저(Chrome, Firefox, Safari, Edge)는 모두 POST 요청에 Origin 헤더를 전송합니다. 차단이 발생하는 경우는 매우 오래된 브라우저나 일부 프록시 환경입니다. 처음 도입 시 false(소프트 모드)로 1~2주 운영하면서 로그를 모니터링한 후 true로 전환하는 것을 권장합니다.
Q. JSON API 요청에서 CSRF Token을 어떻게 전달해야 하나요?
세 가지 방법 중 하나를 사용하세요.
// 방법 1: X-CSRF-Token 헤더 (권장)
fetch('/api/comment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ content: '...' })
});
// 방법 2: JSON body에 포함
fetch('/api/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: '...', _csrf: getCsrfToken() })
});
// 방법 3: URL 파라미터 — 절대 사용 금지 (CSRF-021 위반)
// fetch('/api/comment?_csrf=TOKEN', ...) ← 금지
Q. GET 상태변경 패턴에 정상 GET URL이 걸리면 어떻게 하나요?
/delete-preview 같은 읽기 전용 URI가 패턴에 걸릴 수 있습니다. $GLOBALS['_dx_csrf_get_state_patterns']에서 해당 패턴을 제거하거나, URI에 /view/delete 처럼 명확히 조회임을 나타내는 구조로 변경하세요. 또는 해당 URI를 C8 예외 목록에 추가하되 실제로 상태를 변경하지 않는지 코드 레벨에서 확인하세요.
Q. 관리자 재인증 Step-Up CSRF 강화는 dx-security-hijacking 없이도 동작하나요?
동작합니다. 단, __shj_admin_reauth_required 플래그 검증은 dx-security-hijacking에서 설정하는 값입니다. 해당 플러그인 없이는 이 조건이 검사되지 않으며, Origin 검증만으로 Step-Up이 동작합니다. 완전한 Step-Up을 위해서는 두 플러그인을 함께 사용하세요.
DX Security — CSRF 방어 v1.0.0 — DesignOneX
https://designonex.com
라이선스
디자인원엑스 라이선스 참고
https://designonex.com/notice/view/1787577858493247