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

DX Security — CSRF 보안 플러그인

D DX관리자
2026.09.01 23:00 6 0



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 완전 준수


목차

  1. CSRF란 무엇인가
  2. DXCMS 코어의 현재 CSRF 방어 수준
  3. 코어가 제공하지 않는 것 — 이 플러그인의 역할
  4. 설치 방법
  5. 방어층 상세 설명
    • 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 허용목록 관리
  6. CSRF Security Pipeline 전체 흐름
  7. 훅 실행 타이밍
  8. 보안 로그 읽는 법
  9. 보안 강점 요약 — 기업 납품 요구사항 준수 현황
  10. 방어하지 못하는 영역
  11. 값 변경 방법
  12. CSRF 예외 Endpoint 등록 방법
  13. 다른 플러그인·테마에서 연동하기
  14. 자주 묻는 질문

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.comexample.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

 

첨부파일 1개 회원전용

댓글0

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