
DX Security Guard v2.1.0 — 안내서
DesignOneX CMS 세션 보안 플러그인
PHP 5.6+ · IIS / Apache / Nginx · 공유호스팅 완전 호환
코어(Auth.php / Secure.php / index.php) 수정 없음 — 훅으로만 동작
목차
- 이 플러그인이 하는 일
- v2.1.0 변경 내역
- 설치 방법
- 기능별 상세 설명
- 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)
- 훅 실행 타이밍 전체 흐름
- 보안 로그 읽는 법
- 보안 강점 요약 — 어디까지 방어되는가
- 방어하지 못하는 영역
- 타임아웃·회전 값 변경 방법
- 다른 플러그인에서 session_version 연동하기
- 자주 묻는 질문
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_top은 index.php의 DxExtend::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_top → dx_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