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

DX Security Guard v2.1.0

D DX관리자
2026.09.01 02:42(수정됨) 11 0 7



DX Security Guard v2.1.0 — 안내서

DesignOneX CMS 세션 보안 플러그인
PHP 5.6+ · IIS / Apache / Nginx · 공유호스팅 완전 호환
코어(Auth.php / Secure.php / index.php) 수정 없음 — 훅으로만 동작



목차

  1. 이 플러그인이 하는 일
  2. v2.1.0 변경 내역
  3. 설치 방법
  4. 기능별 상세 설명
    • 4-1. Session Fixation 차단 (P0)
    • 4-2. loginById() 경로 보호 (P0)
    • 4-3. 로그아웃 세션 완전 파기 (P0)
    • 4-4. 모든 요청 공통 검증 — dx_extend_top (P0)
    • 4-5. session_regenerate_id 실패 감지 (P1)
    • 4-6. Idle Timeout / Absolute Timeout (P1)
    • 4-7. session_version — 서버 측 세션 강제 무효화 (P1)
    • 4-8. 주기적 세션 ID 회전 (P1)
    • 4-9. 비밀번호·권한 변경 연동 (P1)
    • 4-10. IP 처리 통합 — Secure::clientIp() 재사용 (P1)
    • 4-11. SameSite 쿠키 PHP 5.6 호환 (P1)
    • 4-12. Session Risk Engine (P2)
  5. 훅 실행 타이밍 전체 흐름
  6. 보안 로그 읽는 법
  7. 보안 강점 요약 — 어디까지 방어되는가
  8. 방어하지 못하는 영역
  9. 타임아웃·회전 값 변경 방법
  10. 다른 플러그인에서 session_version 연동하기
  11. 자주 묻는 질문


1. 이 플러그인이 하는 일

DXCMS 코어는 세션을 시작하고 유지하는 기본 기능만 갖추고 있습니다.
DX Security Guard는 그 위에 세션 생명주기 보안 전반을 추가합니다.

구분 코어(Secure.php) DX Security Guard 추가
세션 시작
세션 쿠키 (Secure/HttpOnly/SameSite)
CSRF 토큰
WAF / IP 차단 / Rate Limit
로그인 시 세션 ID 교체 ✅ P0
loginById() 경로 세션 보호 ✅ P0 (v2.1 신규)
모든 요청 공통 검증 (API 포함) ✅ P0 (v2.1 신규)
regenerate 실패 시 로그인 중단 ✅ P1 (v2.1 신규)
로그아웃 시 완전 파기 부분적 ✅ P0
유휴/절대 타임아웃 ✅ P1
서버 측 세션 강제 무효화 ✅ P1
주기적 세션 ID 회전 24시간 1회 ✅ 30분마다
비밀번호 변경 → 전 기기 로그아웃 ✅ P1
IP 처리 — 코어 신뢰프록시 목록 통합 ✅ P1 (v2.1 신규)
SameSite PHP 5.6 호환 ✅ P1 (v2.1 신규)
이상 징후 위험 점수 계산 ✅ P2


2. v2.1.0 변경 내역

P0 — 필수 수정

loginById() 경로 보호 추가

코어 Auth::loginById()는 소셜 로그인, 관리자 대리 로그인 등에서 사용됩니다.
그러나 이 메서드는 dx_after_login 훅을 실행하지 않아 Session Fixation 방어가 빠져있었습니다.

v2.1.0 해결: dx_after_social_login 훅과 dx_after_login_by_id 훅을 추가 등록하여 동일한 보호를 적용합니다.

v2.0.0: Auth::login() 경로만 보호
v2.1.0: Auth::login() + loginById() 경로 모두 보호

dx_extend_top 연결 — API·관리자 요청도 검증

dx_top 훅은 테마의 _body_top.php에서만 실행됩니다. 즉 다음 요청들은 검증이 빠졌습니다.

  • core/api/ 경로의 API 요청
  • 관리자 페이지 (/admin/)
  • 테마를 거치지 않는 모든 라우트

dx_extend_topindex.phpDxExtend::runTop()에서 라우팅 전 항상 실행됩니다.

v2.1.0 해결: dx_extend_top에 검증 로직을 연결하여 모든 요청에서 Timeout·session_version·Risk를 검사합니다. dx_top은 이중 실행 방지 로직(1초 이내 재호출 스킵)을 추가해 유지합니다.

v2.0.0: 테마를 통한 일반 페이지 요청만 검증
v2.1.0: API, 관리자, 일반 페이지 — 모든 요청 검증


P1 — 강력 권장 수정

session_regenerate_id() 실패 감지

session_regenerate_id()는 드물지만 실패할 수 있습니다 (파일시스템 권한 문제, Redis 연결 오류 등). 실패를 감지하지 않으면 구 세션 ID가 그대로 인증 세션으로 유지됩니다.

v2.1.0 해결:

  • false 반환 여부 확인
  • 재생성 전후 세션 ID가 동일한지 추가 확인
  • 실패 시: 세션에서 사용자 데이터 제거 → LOGIN_ABORT 로그 → false 반환 (로그인 중단)
v2.0.0: 실패 여부 무관하게 로그인 진행
v2.1.0: 실패 감지 → 불완전 로그인 상태 방지 → 안전 처리

IP 처리 — Secure::clientIp() 재사용

v2.0.0에서 플러그인이 독자적인 IP 추출 로직을 갖고 있었습니다. 코어의 신뢰 프록시 목록(Cloudflare 17개 IP 대역 포함)이 적용되지 않아 프록시 환경에서 IP가 다르게 판단될 수 있었습니다.

v2.1.0 해결: _dx_sg_ip()Secure::clientIp()를 직접 호출합니다. 코어와 완전히 동일한 신뢰 프록시 판정이 적용됩니다. 또한 Risk Engine에서 IP를 세션에 저장할 때 원문 대신 md5() 해시로 저장합니다.

SameSite 쿠키 PHP 5.6 호환

setcookie()samesite 옵션 파라미터는 PHP 7.3+에서만 지원됩니다. v2.0.0에서는 PHP 5.6~7.2 환경의 로그아웃 쿠키 삭제 시 SameSite 속성이 적용되지 않았습니다.

v2.1.0 해결: PHP 버전에 따라 분기합니다.

PHP 7.3+: setcookie(name, '', ['samesite' => 'Lax', ...])
PHP 5.6~7.2: setcookie(name, '', time()-1, '/; SameSite=Lax', ...)
              → path 끝에 "; SameSite=Lax" 를 직접 주입하는 호환 방식


3. 설치 방법

3-1. 파일 배치

ZIP 압축 해제 후 DXCMS 루트에 그대로 복사합니다.

(DXCMS 루트)/
└── plugins/
    └── dx-security-guard/
        ├── manifest.php
        └── plugin.php        ← 모든 보안 로직


3-2. 관리자 페이지에서 활성화

관리자  플러그인  dx-security-guard  활성화

활성화 버튼을 누르는 순간부터 모든 보안 기능이 즉시 동작합니다.


3-3. 자동 DB 스키마 적용

활성화 후 첫 번째 페이지 로드 시 자동으로 실행됩니다.

ALTER TABLE `dx_members`
  ADD COLUMN `session_version` INT UNSIGNED NOT NULL DEFAULT 0;
  • 이미 컬럼이 있으면 완전히 스킵 (멱등성 보장)
  • 적용 완료 후 data/sg_v2_installed 파일 생성 → 이후 요청에서 SHOW COLUMNS 생략
  • 컬럼 추가 실패해도 사이트는 정상 운영 (session_version 검증만 비활성화)


4. 기능별 상세 설명


4-1. Session Fixation 차단 (P0)

공격 원리

공격자 → 세션 ID "ABC" 확보
공격자 → 피해자에게 "ABC"가 심어진 링크 전송
피해자 → "ABC" 세션으로 로그인
취약한 시스템 → "ABC" 그대로 인증 세션으로 승격
공격자 → "ABC"로 피해자 계정 접근 성공

이 플러그인의 방어

피해자 → "ABC" 세션으로 로그인 성공
  ↓
dx_after_login 훅 실행 (priority 1 — 가장 먼저)
  ↓
_dx_sg_regenerate(true) 호출
  session_write_close()        기존 세션 파일 잠금 해제 (IIS 호환)
  session_start()
  session_regenerate_id(true)  새 ID "XYZ" 발급 + "ABC" 파일 즉시 삭제
  실패 감지 → 실패 시 로그인 중단 (v2.1 신규)
  ↓
공격자가 가진 "ABC" → 서버에 존재하지 않음 → 접근 불가

언제 동작하는가

  • 로그인이 성공한 직후 1회
  • Auth::login()dx_after_login 훅 → 이 플러그인 (priority 1)

로그인 완료 시 세션에 기록되는 메타

__sg_verified  로그인 시각 (이 값이 있어야 검증 대상 세션)
__sg_login_at  로그인 시각 (Absolute Timeout 기준)
__sg_last_act  마지막 활동 시각 (Idle Timeout 기준)
__sg_rotated   마지막 ID 회전 시각
__sg_sv        session_version DB 캐시 값
__sg_last_ip   로그인 시 IP의 md5 해시 (Risk Engine용)
__sg_last_ua   로그인 시 UA의 md5 해시 (Risk Engine용)


4-2. loginById() 경로 보호 (P0) — v2.1 신규

Auth::loginById()는 다음 경로에서 사용됩니다.

  • 소셜 로그인 (카카오, 네이버, 구글 등)
  • Remember Me 쿠키 자동 로그인
  • 관리자가 특정 회원 대리 로그인

이 메서드는 코어에서 dx_after_login 훅을 실행하지 않습니다. v2.0.0에서는 이 경로가 Fixation 방어에서 완전히 빠져있었습니다.

v2.1.0 해결: 두 가지 추가 훅을 연결합니다.

dx_after_social_login  → 소셜 로그인 완료 시
dx_after_login_by_id   → 향후 코어에서 추가될 훅 대비

두 훅 모두 _dx_sg_on_login($user) 공통 함수를 호출하여 dx_after_login과 동일한 처리를 합니다.

소셜 로그인 플러그인 개발자 안내: 소셜 로그인 완료 후 반드시 다음을 호출하세요.

dx_run_hook('dx_after_social_login', array('user' => $user));


4-3. 로그아웃 세션 완전 파기 (P0)

DXCMS 코어의 기본 로그아웃

// 코어 Auth::logout() 이 하는 일
unset($_SESSION[$this->sessionKey()]);  // 사용자 데이터만 제거

세션 파일은 서버에 남아있고, 세션 쿠키도 브라우저에 남습니다.

이 플러그인의 완전 파기

dx_after_logout 훅 실행 (priority 1)
  ↓
$_SESSION = array()              세션 데이터 전체 삭제
setcookie(세션명, '', 과거)      브라우저 쿠키 즉시 만료
  → PHP 7.3+: samesite 파라미터 사용
  → PHP 5.6~7.2: path에 "; SameSite=Lax" 직접 주입 (v2.1 신규)
session_destroy()                서버 세션 파일/Redis 키 삭제
session_regenerate_id(true)      새 빈 세션 ID 발급


4-4. 모든 요청 공통 검증 — dx_extend_top (P0) — v2.1 신규

배경

실행 위치 커버하는 요청
dx_top 테마 _body_top.php 일반 페이지만
dx_extend_top index.php DxExtend::runTop() 모든 요청

v2.0.0에서는 dx_top만 연결되어 있었으므로 다음 요청은 Timeout·session_version·Risk 검증을 받지 못했습니다.

  • core/api/comment.php, core/api/upload.php 등 API 요청
  • admin/ 관리자 페이지
  • 테마 없이 동작하는 커스텀 라우트

v2.1.0 연결 구조

index.php
  └── DxExtend::runTop()
        └── dx_run_hook('dx_extend_top')   ← 플러그인 연결 (priority 1)
              └── _dx_sg_on_top()           ← 검증 실행

테마 _body_top.php
  └── dx_hook_top()
        └── dx_run_hook('dx_top')          ← 플러그인 연결 (priority 1)
              └── last_act 1초 이내면 스킵  ← 이중 실행 방지
              └── _dx_sg_on_top()

같은 요청에서 dx_extend_topdx_top 순으로 모두 실행되는 경우, dx_top__sg_last_act가 1초 이내에 갱신됐으면 스킵합니다.



4-5. session_regenerate_id 실패 감지 (P1) — v2.1 신규

session_regenerate_id()가 실패하는 상황은 드물지만 실재합니다.

  • 세션 저장 디렉토리 권한 문제
  • Redis 연결 순간 단절
  • 디스크 공간 부족

실패 시 구 세션 ID가 그대로 인증 세션에 남는 것을 방지합니다.

실패 판정 조건 (둘 중 하나)

① session_regenerate_id() 반환값 === false
② 재생성 전후 session_id() 값이 동일

실패 시 처리

1. SESSION_REGEN_FAIL 로그 기록 (oldId의 md5 앞 8자리만 기록)
2. $_SESSION에서 사용자 키 삭제 (불완전 로그인 상태 방지)
3. false 반환
4. 호출 측(_dx_sg_on_login)에서 LOGIN_ABORT 로그 기록

중요: 실패가 반복되면 서버 환경(세션 저장 경로 권한, Redis 연결)을 점검하세요.



4-6. Idle Timeout / Absolute Timeout (P1)

두 타임아웃의 차이

구분 Idle Timeout Absolute Timeout
기준 마지막 활동 이후 경과 시간 로그인 이후 총 경과 시간
기본값 30분 8시간
리셋 조건 페이지 접근할 때마다 리셋 리셋 없음 (로그인 시각 고정)
목적 자리 비운 사이 타인 접근 방지 장시간 세션 하이재킹 피해 최소화
상수 DX_SG_IDLE_TIMEOUT DX_SG_ABS_TIMEOUT

언제 동작하는가

  • 모든 요청에서 dx_extend_top 훅으로 검사 (v2.1부터 API 포함)
  • __sg_verified 마커가 있는 로그인 세션에서만 검사
  • 비로그인 방문자는 완전히 무시

만료 시 처리

일반 페이지 요청
  → HTTP 401 + Location: /auth/login?reason=session_expired

AJAX / JSON 요청
  → HTTP 401 + JSON 응답
  { "success": false, "message": "세션이 만료되었습니다.", "code": "SESSION_EXPIRED" }


4-7. session_version — 서버 측 세션 강제 무효화 (P1)

이 기능이 이 플러그인의 핵심입니다.

개념

DB members 테이블
  user_id = 100
  session_version = 5         현재 유효한 버전

세션 (__sg_sv)
  5                           로그인  캐시한 버전

 요청
  DB  5 = 세션 캐시 5  일치  통과

비밀번호 변경 발생
  DB: session_version  6

다음 요청
  DB  6  세션 캐시 5  불일치  세션 즉시 폐기


실제 활용 시나리오

시나리오 1: 공격자가 세션 ID를 탈취한 경우

공격자가 세션 ID 탈취
  ↓ 관리자가 비밀번호 강제 변경
  ↓ session_version 5 → 6
  ↓ 공격자의 다음 요청: 버전 불일치 → 즉시 폐기

시나리오 2: 모든 기기 로그아웃

마이페이지 → 모든 기기 로그아웃
  ↓ _dx_sg_invalidate_user($userId)
  ↓ PC / 모바일 / 태블릿 / 공격자 — 모든 세션 다음 요청에서 폐기

시나리오 3: 관리자가 특정 계정 강제 차단

관리자 → 회원 관리 → 세션 강제 종료
  ↓ _dx_sg_invalidate_user($memberId)
  ↓ 해당 회원의 모든 세션 즉시 무효화

언제 자동으로 호출되는가

  • dx_after_password_change 훅 실행 시 (비밀번호 변경)
  • dx_after_role_change 훅 실행 시 (권한 변경)
  • 개발자가 직접 _dx_sg_invalidate_user($userId) 호출 시


4-8. 주기적 세션 ID 회전 (P1)

탈취된 세션 ID의 유효 시간을 단축합니다.

동작 조건

  • __sg_verified 마커가 있는 로그인 세션
  • AJAX 요청 스킵 — Set-Cookie 적용 전 경쟁 조건 방지
  • 마지막 회전으로부터 DX_SG_ROTATE_INTV(30분) 경과 후 일반 페이지 요청


회전 방식

write_close() → session_start() → regenerate_id(true) → 데이터 복원
실패 시 SESSION_REGEN_FAIL 로그 기록


4-9. 비밀번호·권한 변경 연동 (P1)

비밀번호 변경

dx_after_password_change 훅이 실행되면 자동으로 동작합니다.

비밀번호 변경 성공
  ↓ dx_after_password_change 훅
  ↓ _dx_sg_invalidate_user($userId)
  ↓ DB: session_version++
  ↓ 해당 사용자의 모든 세션 무효화

⚠️ dx_after_password_change 훅은 DXCMS 코어에 아직 없습니다.
비밀번호 변경 처리 코드에서 직접 호출해야 합니다. (§10 참고)


권한 변경

dx_after_role_change 훅이 실행되면 자동으로 동작합니다.

관리자 → 특정 회원 권한 변경
  ↓ dx_after_role_change 훅
  ↓ ① 변경 대상이 현재 본인이면 → 세션 ID 즉시 재생성 (권한 상승 시 세션 분리)
  ↓ ② 모든 경우 → _dx_sg_invalidate_user($userId) → 기존 세션 전부 무효화


4-10. IP 처리 통합 — Secure::clientIp() 재사용 (P1) — v2.1 신규

v2.0.0의 문제

플러그인이 독자적으로 HTTP_CF_CONNECTING_IP, HTTP_X_FORWARDED_FOR 를 처리했습니다. 코어(Secure.php)의 신뢰 프록시 목록(Cloudflare 17개 IP 대역 + 내부망)이 적용되지 않아, 프록시 환경에서 IP가 다르게 판단될 수 있었습니다.


v2.1.0 해결

function _dx_sg_ip() {
    if (class_exists('Secure')) {
        return Secure::getInstance()->clientIp();  // 코어와 동일한 판정
    }
    // Secure 미로드 환경 폴백
    ...
}
  • Risk Engine의 IP 해시 비교가 코어와 동일한 IP 기준으로 동작
  • Cloudflare, 리버스프록시, 내부망 모두 코어와 동일하게 처리
  • IP 원문은 세션에 저장하지 않고 md5() 해시만 저장


4-11. SameSite 쿠키 PHP 5.6 호환 (P1) — v2.1 신규

로그아웃 시 세션 쿠키를 삭제할 때 SameSite 속성을 올바르게 적용합니다.

// PHP 7.3+ — setcookie() 배열 파라미터 지원
setcookie(session_name(), '', [
    'expires'  => time() - 86400,
    'samesite' => 'Lax',
    'secure'   => $isHttps,
    'httponly' => true,
    ...
]);

// PHP 5.6~7.2 — path에 직접 주입하는 호환 방식
$path = '/; SameSite=Lax';
setcookie(session_name(), '', time() - 86400, $path, $domain, $isHttps, true);

HTTPS 판정도 index.php 코어와 동일한 5가지 조건을 사용합니다.

$_SERVER['HTTPS'] !== 'off'
$_SERVER['SERVER_PORT'] === 443
$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
$_SERVER['HTTP_X_FORWARDED_SSL'] === 'on'
$_SERVER['HTTP_CF_VISITOR'] contains 'https'


4-12. Session Risk Engine (P2)

이상 신호에 위험 점수를 부여하고 임계값에 따라 대응합니다.

IP와 UA는 단독으로 세션을 끊지 않습니다.
모바일 사용자는 LTE→Wi-Fi 전환 시 IP가 바뀌고, 브라우저 업데이트로 UA가 바뀔 수 있습니다.
이 값들은 위험 신호로만 사용합니다.

점수 기준

이상 신호 점수 비고
IP 변화 +20 md5 해시 비교 (v2.1: 원문 저장 → 해시 저장)
UA 변화 +10 md5 해시 비교
관리자 페이지(/admin) 접근 +20 관리자 계정일 때만
이상 요청 (UA 없음·헤더 인젝션) +30  

임계값별 조치

점수 조치 로그 타입
0 ~ 29 정상 — 통과
30 ~ 59 경고 — 로그 기록, 세션 유지 SESSION_RISK_WARN
60 ~ 79 재인증 요구__sg_needs_reauth = true 플래그 SESSION_RISK_REAUTH
80 이상 세션 즉시 폐기 SESSION_RISK_DESTROY

재인증 플래그 활용

// 테마 또는 다른 플러그인에서
if (!empty($_SESSION['__sg_needs_reauth'])) {
    // 비밀번호 재입력 모달 표시 등
}


5. 훅 실행 타이밍 전체 흐름

[모든 요청]
      │
      ▼
index.php
  STEP 1: Secure.php 로드, 세션 시작
  STEP 2: DB 연결, 설정 로드
  STEP 3: load_plugins()
            └── dx-security-guard/plugin.php 로드
                  ├── _dx_sg_install()    (최초 1회: session_version 컬럼 추가)
                  └── 훅 7개 등록
                        dx_after_login        (priority 1)
                        dx_after_social_login (priority 1)  ← v2.1 신규
                        dx_after_login_by_id  (priority 1)  ← v2.1 신규
                        dx_after_logout       (priority 1)
                        dx_extend_top         (priority 1)  ← v2.1 신규
                        dx_top                (priority 1)
                        dx_after_password_change (priority 1)
                        dx_after_role_change     (priority 1)
  STEP 4: Auth::getInstance()
  STEP 5: DxExtend::runTop()
            └── dx_extend_top 훅 실행       ← 모든 요청 여기서 검증
                  ├── [P1] Idle Timeout 검사
                  ├── [P1] Absolute Timeout 검사
                  ├── [P1] session_version DB 대조
                  ├── [P2] Risk 점수 계산
                  └── [P1] 주기적 세션 ID 회전 (30분, AJAX 제외)
  STEP 6: 라우팅 + 디스패치
      │
      ▼
[테마 렌더링 — 일반 페이지만]
  _body_top.php
    └── dx_hook_top() → dx_top 훅
          └── last_act 1초 이내면 스킵 (이중 실행 방지)
          └── _dx_sg_on_top()
      │
      ▼
[로그인 요청]
  Auth::login() 성공
    └── dx_after_login (priority 1) → _dx_sg_on_login($user)
          ├── [P0] 세션 ID 재생성 (실패 시 LOGIN_ABORT → 중단)
          ├── 세션 메타 기록
          └── SESSION_REGEN 로그

  Auth::loginById() 또는 소셜 로그인
    └── dx_after_social_login (priority 1) → _dx_sg_on_login($user)
          └── (동일 처리)
      │
      ▼
[로그아웃 요청]
  Auth::logout()
    └── dx_after_logout (priority 1)
          ├── [P0] 세션 완전 파기 (데이터·쿠키·파일)
          └── SESSION_DESTROY 로그


6. 보안 로그 읽는 법

모든 이벤트는 data/security.log 에 기록됩니다.
Secure.php가 쓰는 파일과 동일하므로 한 파일에서 전체 보안 이벤트를 확인할 수 있습니다.


로그 형식

[날짜 시각][SG:이벤트타입][IP:클라이언트IP][UA:유저에이전트] 메시지


이벤트 타입 목록

타입 발생 시점 의미
SG:INSTALL_COLUMN 최초 설치 session_version 컬럼 추가 완료
SG:INSTALL_ERROR 설치 실패 컬럼 추가 실패 (DB 권한 부족 등)
SG:SESSION_REGEN 로그인 성공 세션 ID 재생성 완료
SG:REGEN_FAIL regenerate 실패 세션 ID 재생성 실패 — 서버 환경 점검 필요
SG:LOGIN_ABORT regenerate 실패 안전을 위해 로그인 중단
SG:SESSION_DESTROY 로그아웃 세션 완전 파기 완료
SG:SESSION_ROTATE 30분마다 주기적 세션 ID 회전
SG:SESSION_IDLE_TIMEOUT 유휴 30분 초과 유휴 타임아웃으로 세션 폐기
SG:SESSION_ABS_TIMEOUT 로그인 후 8시간 초과 절대 타임아웃으로 세션 폐기
SG:SESSION_VERSION_MISMATCH 버전 불일치 강제 무효화로 세션 폐기
SG:SESSION_INVALIDATE 비번/권한 변경 session_version++ 실행
SG:SESSION_RISK_WARN Risk 30~59 이상 징후 경고 (세션 유지)
SG:SESSION_RISK_REAUTH Risk 60~79 재인증 요구 플래그 설정
SG:SESSION_RISK_DESTROY Risk 80+ 위험 임계 초과로 세션 폐기
SG:PASSWORD_CHANGE 비밀번호 변경 훅 전 세션 무효화 실행
SG:PRIV_ESCALATE 권한 변경 훅 세션 재생성 + 무효화 실행


로그 예시

[2026-09-01 02:15:33][SG:SESSION_REGEN][IP:121.131.xx.xx][UA:Mozilla/5.0...] member_id=42 fixation방어 — new session ID issued
[2026-09-01 02:15:40][SG:SESSION_ROTATE][IP:121.131.xx.xx][UA:Mozilla/5.0...] member_id=42 periodic rotation
[2026-09-01 03:12:01][SG:SESSION_IDLE_TIMEOUT][IP:121.131.xx.xx][UA:Mozilla/5.0...] member_id=42
[2026-09-01 09:00:11][SG:SESSION_RISK_WARN][IP:175.200.xx.xx][UA:Mozilla/5.0 (iPhone...] member_id=42 score=30 reasons=ip_change
[2026-09-01 09:00:22][SG:SESSION_VERSION_MISMATCH][IP:10.0.0.1][UA:python-requests/2.28] member_id=42 cached=5 db=6
[2026-09-01 10:30:01][SG:REGEN_FAIL][IP:121.131.xx.xx][UA:Mozilla/5.0...] session_regenerate_id failed. oldId_hash=a3f29c11
[2026-09-01 10:30:01][SG:LOGIN_ABORT][IP:121.131.xx.xx][UA:Mozilla/5.0...] session regenerate failed — login aborted for safety

⚠️ 세션 ID 원문은 절대 로그에 기록하지 않습니다. 실패 시 md5 앞 8자리만 기록합니다.



7. 보안 강점 요약 — 어디까지 방어되는가

✅ 완전 방어

공격 유형 방어 방법
Session Fixation (일반 로그인) 로그인 시 세션 ID 교체 + 구 파일 즉시 삭제
Session Fixation (소셜/loginById 경로) dx_after_social_login 훅으로 동일 보호 (v2.1)
로그아웃 후 세션 재사용 완전 파기 (데이터·쿠키·파일 모두 삭제)
API 요청의 세션 검증 우회 dx_extend_top으로 모든 요청 검증 (v2.1)
장시간 방치된 세션 악용 Idle Timeout 30분 자동 만료
8시간 초과 세션 하이재킹 Absolute Timeout 강제 재인증
비밀번호 탈취 후 유효 세션 유지 session_version++ → 기존 세션 전부 무효화
다른 기기 세션 악용 session_version++으로 전 기기 동시 로그아웃
권한 상승 후 구 세션 사용 권한 변경 시 세션 재생성 + 무효화
세션 ID 장시간 재사용 30분마다 자동 회전
불완전 로그인 상태 악용 regenerate 실패 시 로그인 자체 중단 (v2.1)


⚠️ 부분 방어 (탐지·로그만)

공격 유형 현재 대응
IP 기반 세션 하이재킹 점수 +20 → 단독으로는 세션 폐기 안 함 (모바일 고려)
UA 스푸핑 점수 +10 → 단독으로는 세션 폐기 안 함
자동화 봇 스크래핑 UA 없음 +30 → 이상 요청 탐지·로그
관리자 페이지 이상 접근 Risk 점수 가중 → 조합 점수에 따라 대응


8. 방어하지 못하는 영역

공격 유형 이유 필요한 추가 대책
XSS를 통한 세션 쿠키 탈취 탈취 시점을 알 수 없음 HttpOnly 쿠키(Secure.php), CSP 헤더
HTTPS 없는 환경의 네트워크 도청 암호화 계층 부재 HTTPS 적용 필수
서버 측 세션 파일 직접 접근 서버 권한 탈취 시 서버 권한 관리, 세션 디렉토리 보호
SQL Injection 별도 영역 DXCMS 코어 PreparedStatement
CSRF 별도 영역 Secure.php의 csrfToken()
Brute Force 로그인 별도 영역 Secure.php의 Rate Limit
동시 세션 수 제한 미구현 추후 확장 가능


9. 타임아웃·회전 값 변경 방법

plugin.php 상단의 상수를 수정합니다.

define('DX_SG_IDLE_TIMEOUT',  1800);   // 유휴 타임아웃: 초 단위 (기본 30분)
define('DX_SG_ABS_TIMEOUT',   28800);  // 절대 타임아웃: 초 단위 (기본 8시간)
define('DX_SG_ROTATE_INTV',   1800);   // 세션 ID 회전 주기: 초 단위 (기본 30분)
환경 Idle Absolute Rotate
일반 커뮤니티 (기본) 1800 (30분) 28800 (8시간) 1800 (30분)
금융·관리자 전용 600 (10분) 3600 (1시간) 600 (10분)
쇼핑몰 3600 (1시간) 86400 (24시간) 3600 (1시간)


10. 다른 플러그인에서 session_version 연동하기

특정 사용자 세션 강제 무효화

// 어디서든 호출 가능
_dx_sg_invalidate_user($userId);

// 예: 관리자가 회원 강제 차단 후 즉시 로그아웃
$db->query("UPDATE members SET status = 0 WHERE id = ?", array($memberId));
_dx_sg_invalidate_user($memberId);


비밀번호 변경 연동

// 비밀번호 변경 성공 처리 후
$db->query("UPDATE members SET password = ? WHERE id = ?", array($hash, $userId));

// 이 한 줄로 Security Guard가 자동으로 session_version++ 처리
dx_run_hook('dx_after_password_change', array('user_id' => $userId));


권한 변경 연동

$db->query("UPDATE members SET role = ? WHERE id = ?", array($newRole, $userId));

dx_run_hook('dx_after_role_change', array(
    'user_id'  => $userId,
    'old_role' => $oldRole,
    'new_role' => $newRole,
));


소셜 로그인 플러그인 연동 (v2.1 신규)

Auth::loginById()를 사용하는 소셜 로그인 플러그인은 반드시 다음을 호출해야 합니다.

// loginById() 호출 후
$auth->loginById($memberId);

// Security Guard 보호 적용
dx_run_hook('dx_after_social_login', array('user' => $memberData));


11. 자주 묻는 질문

Q. 플러그인을 활성화하면 기존 로그인된 사용자가 강제 로그아웃 되나요?

아니요. __sg_verified 마커가 없는 기존 세션은 Timeout·version 검증 대상에서 제외됩니다. 기존 사용자는 다음 로그인부터 보호됩니다.


Q. IIS Windows 환경에서도 정상 동작하나요?

네. 세션 ID 재생성 시 session_write_close() → session_start() → session_regenerate_id() 순서로 IIS의 세션 파일 잠금 문제를 해결했습니다.


Q. Redis 세션을 사용 중인데 동작하나요?

네. session_regenerate_id(true)는 Redis 세션 핸들러에서도 구 키를 삭제하고 새 키를 생성합니다. session_destroy()도 동일하게 Redis 키를 삭제합니다.


Q. session_version DB 조회가 성능에 영향을 주지 않나요?

session_version은 Primary Key(id) 인덱스로 조회하는 단순 SELECT 1회입니다. 대부분의 환경에서 1ms 이하입니다. 트래픽이 매우 많은 환경에서는 Redis 캐시 레이어 추가를 고려할 수 있습니다.


Q. AJAX 요청에서 세션 ID 회전이 충돌하지 않나요?

회전은 AJAX 요청에서 완전히 스킵합니다. HTTP_X_REQUESTED_WITH: XMLHttpRequest 또는 Accept: application/json 헤더가 있는 요청은 회전 없이 통과합니다.


Q. REGEN_FAIL 로그가 자주 보입니다. 어떻게 해야 하나요?

세션 저장 경로의 권한 문제가 가장 흔한 원인입니다.

파일 세션: data/sessions/ 디렉토리 권한 700 이상 확인
Redis 세션: Redis 연결 안정성 확인
디스크: 서버 디스크 여유 공간 확인

Q. dx_extend_top과 dx_top이 같은 요청에서 모두 실행되면 이중으로 처리되지 않나요?

dx_top 핸들러 내부에서 __sg_last_act 갱신 시간을 확인합니다. dx_extend_top이 먼저 실행되어 이미 갱신했다면 1초 이내이므로 dx_top은 스킵합니다.


DX Security Guard v2.1.0 — DesignOneX
https://designonex.com
 

라이선스

디자인원엑스 라이선스 참고
https://designonex.com/notice/view/1787577858493247

 

 

첨부파일 1개 회원전용

댓글7

안졸리니졸리 2026.09.01 09:58
하수라 내용봐도 잘 모르겠지만
Security Guard 단어만 봐도 일단 좋아보이고 보안에 안심이 됩니다
고생하셨습니다
감사합니다
^____________^
D
DX관리자 2026.09.01 10:30
네 보안패치는 앞으로 이와같은 방식으로 확장. 예정입니다. 감사합니다
안졸리니졸리 2026.09.01 11:14
그누XX는 보안패치 나올때마다 머리 아픈데 여긴 Core 수정없이 보안패치만 하면 되니 아주 편하네요
감사합니다
^_______^
D
DX관리자 2026.09.01 11:49
네 맞습니다
새로운 경험을 제공해 드릴 예정입니다

감사합니다
여
여유당 2026.09.01 11:57

바로 보안플러그인을 배포해 주시네요.
감사합니다.
 

앞서서 봇 차단 플러그인을 배포해 주셨는데
이를 대체해서 이름이 변경된 건인지 / 새로운 플러그인 인지 여부가 안내가 없어서요.

 

향후 이용 시 기존 봇차단 플러그인과 같이 사용해야 하는건지
아님 이 플러그인만 사용하면 되는지 궁금합니다.

D
DX관리자 2026.09.01 12:16
다 별도입니다
모두 사용하길 권장합니다
여
여유당 2026.09.01 12:48

네 알겠습니다.
감사합니다.

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