회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
13. 보안

기본 보안 구조

D DX
2026.05.10 16:12(수정됨) 129 0

1장. 보안 시스템 개요

DXCMS의 보안은 Secure.php(v5.2.2) 하나에 집약되어 있습니다. PHP 5.6+ 환경부터 최신 PHP까지 완전 호환되며, Apache/Nginx/IIS, 공유호스팅, 클라우드 환경 모두에서 동작합니다. Redis가 있으면 Redis 기반으로, 없으면 파일 기반으로 자동 폴백됩니다.


1.1 보안 레이어 구조

모든 HTTP 요청
  ↓
① 직접 접근 차단 (DX_CMS 상수 미정의 시 403 즉시 종료)
  ↓
② IP 차단 체크 (Redis: dx:ban:{IP} 키 확인)
  ↓
③ WAF 검사 (SQL인젝션/XSS/LFI/CMD 패턴 탐지)
  ↓
④ 봇 탐지 (의심 User-Agent 로그 기록)
  ↓
⑤ 세션 보안 설정 (HttpOnly, SameSite, Strict Mode)
  ↓
⑥ 보안 헤더 발행 (X-Frame, CSP, HSTS 등 8가지)
  ↓
⑦ CSRF 토큰 발급/검증
  ↓
⑧ 인증 (Auth::loadSession → HMAC 토큰 검증)
  ↓
⑨ Rate Limit (API별 요청 속도 제한)
  ↓
⑩ 파일 업로드 검증 (MIME, 확장자, 매직바이트)
  ↓
비즈니스 로직 처리


1.2 Secure.php 제공 기능 목록

메서드/기능 설명
initSession(), startSession() 세션 보안 설정 및 시작. HttpOnly, SameSite, 파일 잠금 최적화
sendSecurityHeaders() HTTP 보안 헤더 8종 자동 발행
csrfToken(), csrfField(), csrfCheck() CSRF 토큰 발급/폼 삽입/검증
wafCheck() SQL/XSS/LFI/CMD 패턴 탐지 및 IP 차단
rateLimit() 요청 속도 제한 (Redis/파일 이중 지원)
clientIp() Cloudflare 포함 실제 클라이언트 IP 추출
randomBytes(), randomHex() 암호학적으로 안전한 난수 생성 (PHP 5.6+ 호환)
hashEquals() 타이밍 공격 방지 문자열 비교
bcryptHash(), bcryptVerify() BCrypt 비밀번호 해시/검증
sanitize(), esc(), safeUrl() 입력 정제 및 출력 이스케이프
validateUpload() 파일 업로드 보안 검증 (MIME, 확장자, 매직바이트)
initSecurity() IP차단+WAF+봇탐지 일괄 실행 (선택 호출)
getRedis() Redis 연결 반환 (DxCache 등 내부 공유)


2장. WAF (웹 애플리케이션 방화벽)

wafCheck()는 모든 요청의 GET/POST 파라미터에서 공격 패턴을 탐지합니다. 에디터 본문 등 HTML을 포함하는 필드는 오탐 방지를 위해 검사에서 제외됩니다.


2.1 탐지 패턴 상세

유형 패턴 예시 탐지 대상
SQL UNION ALL SELECT, information_schema SELECT/FROM 조합, 아웃파일, sleep(), benchmark()
XSS <script>, blocked: 스크립트 태그, 이벤트 핸들러, 자바스크립트 URL
LFI ../../, /etc/passwd, php:// 디렉토리 탐색, 시스템 파일 접근, PHP 래퍼
CMD ; cat, || whoami, && id 명령어 연결자(;, ||, &&) + 시스템 명령어 조합


2.2 검사 대상 및 제외 필드

// 검사 대상
$_GET   → 모든 파라미터 전체 검사
$_POST  → 아래 제외 필드를 제거한 후 검사
$_COOKIE → 세션 쿠키 오탐 방지를 위해 완전 제외

// POST 본문 제외 필드 (에디터 HTML 포함 필드)
'content', 'body', 'description', 'editor_content',
'comment', 'message', 'text', 'detail', 'intro',
'post_content', 'reply', 'html', 'summary'

// 이유: CKEditor 등 에디터에서 작성한 HTML에
// <script> 태그나 SQL 키워드가 포함될 수 있음
// → 정상적인 콘텐츠가 WAF에 차단되는 오탐 방지


2.3 차단 처리 흐름

WAF 패턴 탐지
  ↓
① secLog("WAF", "Blocked: sql | URI: /write")
   → data/security.log 에 기록
   형식: [2026-05-11 00:00:00][WAF][IP:1.2.3.4][UA:Mozilla...] Blocked: sql | URI: ...
  ↓
② block("WAF_SQL") 호출
  ↓
③ Redis::setex("dx:ban:{IP}", 1800, "WAF_SQL")
   → 해당 IP를 30분간 차단 등록
  ↓
④ HTTP 403 + "<h1>403 Forbidden</h1>" 출력 후 exit

// Redis 없는 환경
→ 로그만 기록, IP 차단 기능 없음
→ 파일 기반 Rate Limit으로 보완 가능


2.4 WAF 커스터마이징

플러그인이나 extend/ 파일에서 wafExcludeFields를 확장하거나 wafRules에 패턴을 추가할 수 있습니다.
// extend/top/01_custom_waf.php — WAF 제외 필드 추가 예시
if (!defined("DX_CMS")) exit;

$secure = Secure::getInstance();

// Reflection으로 wafExcludeFields에 필드 추가 (PHP 5.6+)
// 또는 자체 WAF 훅 구현
dx_add_hook("dx_waf_exclude_fields", function($fields) {
    $fields[] = "custom_editor_field";
    $fields[] = "my_html_content";
    return $fields;
});

// IP 수동 차단 (Redis 있을 때)
$redis = Secure::getRedis();
if ($redis) {
    $redis->setex("dx:ban:1.2.3.4", 86400, "MANUAL"); // 24시간 차단
}


3장. CSRF (크로스 사이트 요청 위조) 보호

CSRF 공격은 사용자의 인증된 세션을 악용해 악의적인 요청을 보내는 공격입니다. DXCMS는 세션 기반 토큰으로 모든 폼 제출을 검증합니다.


3.1 CSRF 토큰 구조

// 세션에 저장되는 구조
$_SESSION["dx_csrf"] = array(
    "token"  => "64자 랜덤 hex 문자열",
    "expire" => time() + 10800,   // 3시간 유효
);

// 상수 CSRF_TTL = 10800 (3시간)
// 세션 수명(2시간)보다 길어서 세션 갱신 중에도 유효

// 토큰 유지 전략 (v6.2.0)
// 토큰이 없거나 완전 만료된 경우만 새로 생성
// 토큰이 있으면 expire만 연장 (토큰 값 유지)
// → 연속 AJAX 요청에서도 동일 토큰 사용 가능


3.2 토큰 발급 및 폼 삽입

// 방법 1: hidden input 필드 직접 삽입 (폼 안에서)
<?php echo dx_csrf_field(); ?>
// 출력: <input type="hidden" name="_csrf" value="abc123...">

// 방법 2: 토큰 값만 가져오기
$token = dx_csrf_token();

// 방법 3: HTML <meta> 태그 (layout/main.php에서)
<meta name="csrf-token" content="<?php echo dx_csrf_token(); ?>">

// AJAX 요청에서 사용
fetch("/api/comment", {
    method: "POST",
    headers: {
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
        "X-Requested-With": "XMLHttpRequest"
    },
    body: formData
})


3.3 CSRF 검증

// POST 처리 핸들러에서 반드시 호출
dx_csrf_check();  // = Secure::getInstance()->csrfCheck()

// 검증 순서
1. $_POST["_csrf"] 또는 HTTP_X_CSRF_TOKEN 헤더에서 토큰 읽기
2. $_SESSION["dx_csrf"]["token"]과 hash_equals() 비교 (타이밍 공격 방어)
3. expire 확인 (3시간 초과 시 실패)

// 검증 실패 시 처리 (3가지 케이스)

// ① AJAX 요청 (X-Requested-With: XMLHttpRequest)
→ HTTP 403 + JSON: {"success":false,"message":"세션이 만료되었습니다..."}

// ② /auth/login, /auth/register 페이지
→ 세션에서 토큰 삭제 + GET redirect → 새 토큰으로 폼 재렌더링
→ 사용자에게 폼이 다시 표시됨 (새로고침 효과, UX 보호)

// ③ 일반 POST 페이지
→ HTTP 403 + 세션 만료 모달 HTML 출력
→ "로그인 연장" 팝업 버튼 (PC) 또는 직접 이동 (모바일)
→ "로그아웃" 버튼, "뒤로가기" 버튼 제공

💡 CSRF + SameSite 이중 방어
토큰 검증: 모든 POST 요청에 _csrf 필드 필수
  → 타 사이트에서 폼을 제출해도 토큰을 모르면 실패

SameSite=Lax 쿠키: 외부 사이트의 POST 폼에서 세션 쿠키 미전송
  → 세션 쿠키 자체가 전달되지 않아 2중 차단

단, GET 요청은 SameSite=Lax에서 쿠키 전송 허용
  → 링크 클릭 UX 유지 (GET은 원래 부작용 없어야 함)


4장. HTTP 보안 헤더 (sendSecurityHeaders)

모든 응답에 자동으로 발행됩니다. .htaccess 또는 웹서버 설정 없이도 동작하므로 공유호스팅 환경에서도 보안이 유지됩니다.


4.1 발행되는 보안 헤더 전체

헤더 값 및 역할
X-Powered-By (제거) — PHP/Apache 버전 정보 노출 차단. 공격자 정보 수집 방해
X-Frame-Options SAMEORIGIN — 클릭재킹 방어. 동일 도메인 iframe만 허용. 타 사이트에서 iframe 삽입 차단
X-Content-Type-Options nosniff — MIME 스니핑 방어. Content-Type 외 실행 차단 (JS/CSS 위장 파일 방어)
Referrer-Policy strict-origin-when-cross-origin — 외부 사이트로 이동 시 경로 정보 미전송. 내부 URL 구조 노출 방지
X-XSS-Protection 1; mode=block — 구형 IE/Chrome의 XSS 필터 활성화. 의심 스크립트 감지 시 페이지 렌더링 차단
Permissions-Policy camera=(), microphone=(), geolocation=() — 카메라/마이크/위치 API 접근 차단. 브라우저 권한 악용 방지
Strict-Transport-Security max-age=31536000; includeSubDomains (HTTPS만) — 1년간 HTTPS 강제. HTTP 다운그레이드 공격 방어
Content-Security-Policy XSS 방어. 허용된 소스에서만 리소스 로드. 인라인 스크립트 제한 (아래 상세 참고)


4.2 CSP (Content-Security-Policy) 상세

CSP는 브라우저가 어떤 소스의 리소스만 로드할 수 있는지 제어합니다. XSS 공격에서 공격자가 삽입한 스크립트의 실행을 차단하는 최후 방어선입니다.
default-src 'self';
  → 기본: 같은 도메인 리소스만 허용

script-src 'self' 'unsafe-inline' 'unsafe-eval' https: blob:;
  → 인라인 스크립트, CDN(jQuery, Bootstrap), Blob URL 허용
  → unsafe-inline 필요 이유: 레거시 jQuery 플러그인 등 인라인 사용

style-src 'self' 'unsafe-inline' https: data:;
  → 인라인 스타일, 외부 CSS CDN, Data URI 허용

img-src 'self' data: https: blob:;
  → 외부 이미지, Base64 Data URI, Blob URL 허용

font-src 'self' data: https:;
  → Google Fonts 등 외부 웹폰트 허용

connect-src 'self' https: wss: ws:;
  → AJAX, Fetch, WebSocket(소켓 실시간 기능) 허용
  → wss:// 추가 이유: dx-socket 플러그인 WebSocket 연결 지원

frame-src 'self' https:;
  → 소셜 로그인 팝업, 외부 iframe 허용

object-src 'none';
  → Flash, ActiveX, 플러그인 완전 차단

⚠️ unsafe-inline 설정 이유
script-src에 'unsafe-inline'이 포함된 이유:
① CKEditor 4 등 레거시 에디터가 인라인 스크립트를 생성
② 테마의 layout/main.php 등에서 PHP 변수를 JS에 주입하는 패턴 사용
③ jQuery 플러그인 중 nonce 미지원 구버전 다수

'unsafe-inline'을 제거하려면 모든 인라인 스크립트에 nonce 적용 필요.
getCspNonce() 메서드로 CSP nonce를 생성하여 <script nonce="..."> 방식 사용 가능.


4.3 HSTS (Strict-Transport-Security) 동작

// HTTPS 환경에서만 발행
// $isHttps = (!empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off")
//         || $_SERVER["SERVER_PORT"] === "443"

Strict-Transport-Security: max-age=31536000; includeSubDomains

// 효과:
// 브라우저가 1년간 이 도메인 + 서브도메인에 HTTP 접속 시도를 자동으로 HTTPS로 변환
// → HTTP 다운그레이드 공격(SSLStrip) 방어
// → 첫 HTTPS 접속 후부터 적용됨


5장. Rate Limiting (요청 속도 제한)

동일 IP나 동일 키에서 짧은 시간에 과다 요청이 오면 차단합니다. 무차별 대입 공격(brute force), 스팸 등록, DoS 공격을 방어합니다.


5.1 rateLimit() 메서드

// 기본 사용법
$secure = Secure::getInstance();

// rateLimit($key, $limit, $window, $ipLimit)
// $key:     식별 키 (기능별로 다르게 설정)
// $limit:   $window 초 내 최대 요청 수
// $window:  측정 시간 윈도우 (초)
// $ipLimit: IP별 추가 제한 (0=IP 제한 없음)

// 로그인 시도 제한 (30초에 5회)
if (!$secure->rateLimit("login_attempt", 5, 30)) {
    dx_json(array("success"=>false, "message"=>"잠시 후 다시 시도해주세요."), 429);
}

// 댓글 작성 제한 (60초에 10회, IP별 30회)
if (!$secure->rateLimit("comment_write", 10, 60, 30)) {
    dx_json(array("success"=>false, "message"=>"너무 빠르게 작성하고 있습니다."), 429);
}

// 파일 업로드 제한 (10분에 20회)
if (!$secure->rateLimit("file_upload", 20, 600)) {
    dx_json(array("success"=>false, "message"=>"업로드 횟수 초과."), 429);
}


5.2 Redis 기반 슬롯 방식

// Redis 기반 Rate Limit 원리

// 시간을 $window 크기의 슬롯으로 분할
$slot = floor(time() / $window);  // 현재 슬롯 번호
$sKey = "rl:{$key}:{$slot}";      // Redis 키

// 카운터 증가
$count = $redis->incr($sKey);     // 원자적 증가 (동시성 안전)
if ($count === 1) {
    $redis->expire($sKey, $window + 5);  // 슬롯 만료 설정
}

// 초과 여부 확인
if ($count > $limit) return false;  // 차단

// 장점:
// - incr()가 원자적 연산 → 동시 요청에서 카운터 정확
// - 슬롯 만료로 자동 정리 → 메모리 증가 없음
// - Redis 오류 시 요청 허용 (가용성 우선)


5.3 파일 기반 폴백

// Redis 없는 환경에서 파일 기반 Rate Limit

// 저장 위치: data/cache/rl_{md5(key+ip)}.tmp
$file = DX_ROOT . "/data/cache/rl_" . md5($key . $ip) . ".tmp";

// 파일 내용: serialize(["count"=>N, "start"=>timestamp])
// 윈도우 시간이 지나면 카운터 초기화
// LOCK_EX로 파일 잠금 → 동시성 보장

// 주의: 트래픽이 많으면 파일 I/O 부하 증가
// → 고트래픽 환경에서는 Redis 사용 권장


5.4 실전 적용 예시

적용 위치 권장 설정
로그인 API rateLimit("login", 5, 30) — 30초에 5회. 무차별 대입 방어
회원가입 API rateLimit("register", 3, 60) — 60초에 3회. 대량 계정 생성 방어
댓글 작성 rateLimit("comment", 10, 60, 20) — 60초에 10회/IP 20회
게시글 작성 rateLimit("write", 5, 60) — 60초에 5회
파일 업로드 rateLimit("upload", 20, 600) — 10분에 20회
소셜 로그인 시작 rateLimit("oauth_start", 10, 60) — 60초에 10회
비밀번호 찾기 rateLimit("pw_reset", 3, 300) — 5분에 3회


6장. IP 처리 및 차단


6.1 실제 클라이언트 IP 추출 (clientIp)

Cloudflare, 리버스 프록시 환경에서도 실제 클라이언트 IP를 정확히 추출합니다.
// clientIp() 동작 흐름

1. REMOTE_ADDR이 신뢰 프록시 범위인지 확인
   신뢰 프록시: 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
   Cloudflare: 173.245.48.0/20, 103.21.244.0/22 등 14개 대역

2. 신뢰 프록시인 경우:
   ① HTTP_CF_CONNECTING_IP (Cloudflare 전용) 우선 사용
   ② HTTP_X_FORWARDED_FOR 첫 번째 IP 사용

3. 신뢰 프록시가 아닌 경우:
   → REMOTE_ADDR 직접 사용

// CIDR 범위 체크: inet_pton()으로 IPv6도 지원
$ip = Secure::getInstance()->clientIp();
$ip = dx_ip(); // 헬퍼 함수


6.2 IP 차단 구조

// Redis 기반 IP 차단 (WAF 탐지 시 자동 등록)

// 차단 등록 (30분)
$redis->setex("dx:ban:{IP}", 1800, "WAF_SQL");

// 차단 확인 (매 요청 checkBan())
if ($redis->get("dx:ban:{IP}")) {
    http_response_code(403);
    exit("<h1>403 Forbidden</h1>");
}

// 수동 IP 차단 (관리자 또는 extend/top/ 파일에서)
$redis = Secure::getRedis();
if ($redis) {
    // 특정 IP 24시간 차단
    $redis->setex("dx:ban:1.2.3.4", 86400, "MANUAL");

    // 차단 해제
    $redis->del("dx:ban:1.2.3.4");

    // 차단된 모든 IP 조회
    $keys = $redis->keys("dx:ban:*");
}

⚠️ Redis 없는 환경에서 IP 차단
Redis가 없으면 IP 차단 기능이 동작하지 않습니다.
WAF 패턴을 탐지해도 로그만 기록되고 차단되지 않습니다.
해결 방법:
1. Redis 설치 후 data/config.php에 REDIS_HOST 정의
2. Apache: .htaccess의 Deny from 지시자 활용
3. Nginx: deny 지시자 활용
4. extend/top/ 파일에서 IP 기반 exit 처리


7장. 파일 업로드 보안 (validateUpload)

파일 업로드는 공격자가 악성 파일을 서버에 올리는 주요 경로입니다. DXCMS는 3단계 검증으로 악성 파일을 차단합니다.


7.1 3단계 검증 흐름

validateUpload($tmpPath, $originalName, $maxBytes)
  ↓
① 파일 크기 검증
   - 파일 존재 및 읽기 가능 확인
   - 빈 파일(0바이트) 차단
   - $maxBytes 초과 차단 (0이면 무제한)
  ↓
② MIME 타입 검증 (실제 파일 내용 기반)
   - finfo_open(FILEINFO_MIME_TYPE) 우선 사용
   - 없으면 mime_content_type() 폴백
   - 허용 MIME 화이트리스트와 비교
  ↓
③ 파일 확장자 검증
   - 위험 확장자 블랙리스트와 비교
   - php, php3~php5, phtml, phar, asp, aspx,
     jsp, exe, dll, bat, sh, cgi, pl, htaccess 등 차단
  ↓
④ 이미지 매직바이트 검증 (이미지 파일인 경우만)
   - 파일 앞 12바이트로 실제 이미지 형식 확인
   - JPEG: FF D8 FF
   - PNG: 89 50 4E 47 0D 0A 1A 0A
   - GIF: 47 49 46 38 37/39 61
   - WebP: 52 49 46 46 (RIFF)
   - 매직바이트 불일치 → 위장 파일로 차단


7.2 허용 MIME 타입 화이트리스트

// 기본 허용 MIME 타입
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp',
'application/pdf',
'application/msword',                    // .doc
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.ms-excel',              // .xls
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/zip', 'application/x-zip-compressed',
'text/plain',

// 추가가 필요한 경우 플러그인에서
// 업로드 API에서 MIME 검사 전 허용 목록 확장


7.3 사용 예시

// 파일 업로드 처리 예시
if (!empty($_FILES["attachment"]["tmp_name"])) {
    $maxBytes = (int)dx_config("upload_max_size", 10485760); // 10MB 기본

    $result = Secure::validateUpload(
        $_FILES["attachment"]["tmp_name"],
        $_FILES["attachment"]["name"],
        $maxBytes
    );

    if (!$result["ok"]) {
        dx_json(array("success"=>false, "message"=>$result["error"]));
    }

    // $result["mime"]  → 실제 MIME 타입
    // $result["size"]  → 파일 크기 (bytes)
    // $result["ext"]   → 원본 확장자

    // 저장 파일명 안전 처리
    $safeName = Secure::sanitize($_FILES["attachment"]["name"], "filename");
    $saveName = date("Ymd") . "_" . Secure::randomHex(8) . "." . $result["ext"];
    move_uploaded_file($_FILES["attachment"]["tmp_name"], DX_ROOT."/data/uploads/".$saveName);
}


8장. 암호화 유틸리티


8.1 안전한 난수 생성

// randomBytes($length) — 암호학적 안전 난수
// PHP 7+: random_bytes() 사용 (CSPRNG)
// PHP 5.6: openssl_random_pseudo_bytes() 폴백
// 최후 폴백: mt_rand() (암호학적으로 약함, 비상용)

$bytes = Secure::randomBytes(32);    // 32바이트 랜덤
$hex   = Secure::randomHex(64);      // 64자 hex 문자열

// 사용 사례
$csrfToken  = Secure::randomHex(64); // CSRF 토큰
$remToken   = Secure::randomHex(64); // Remember Me 토큰
$rescueKey  = bin2hex(Secure::randomBytes(16)); // 32자 rescue key
$uploadName = Secure::randomHex(8);  // 업로드 파일명


8.2 타이밍 공격 방지 비교 (hashEquals)

일반 문자열 비교(===, strcmp)는 앞에서부터 비교하다 불일치 시 즉시 종료됩니다. 공격자는 응답 시간의 미세한 차이로 토큰을 추측할 수 있습니다. hashEquals는 항상 전체 문자열을 비교하여 시간 차이를 없앱니다.
// 취약한 비교 (타이밍 공격 가능)
if ($storedToken === $userToken) { ... }

// 안전한 비교 (타이밍 공격 방어)
if (Secure::hashEquals($storedToken, $userToken)) { ... }

// PHP 5.6 호환 구현
// hash_equals() 있으면 사용
// 없으면 XOR 비교 (상수 시간 O(n))
$result = 0;
for ($i = 0; $i < strlen($known); $i++) {
    $result |= ord($known[$i]) ^ ord($user[$i]);
}
return $result === 0; // 모든 바이트가 일치하면 0

// 배열 인자 지원 (upload.php 하위 호환)
// hashEquals(["token"=>"abc"], "abc") → 자동으로 token 키 추출


8.3 BCrypt 비밀번호 해시

// 비밀번호 해시 생성 (회원가입/비밀번호 변경)
$hash = Secure::bcryptHash("사용자비밀번호");
// PHP 7+: password_hash(PASSWORD_BCRYPT, cost=10)
// PHP 5.6: crypt() 폴백

// 비밀번호 검증 (로그인)
if (Secure::bcryptVerify("입력비밀번호", $storedHash)) {
    // 로그인 성공
}
// PHP 7+: password_verify()
// PHP 5.6: crypt() 비교 폴백

// BCrypt 특징
// - 솔트 자동 포함 → 레인보우 테이블 무력화
// - 비용 인수(cost=10) → 느린 해시로 무차별 대입 방어
// - 동일 비밀번호도 매번 다른 해시 생성


9장. 입력 정제 및 출력 이스케이프

입력 정제(sanitize)와 출력 이스케이프(esc)는 XSS 공격의 핵심 방어입니다. 데이터는 입력 시 정제하고 출력 시 이스케이프하는 것이 원칙입니다.


9.1 sanitize() — 입력 정제

타입 처리 방식 사용 예
text (기본) htmlspecialchars ENT_QUOTES 제목, 이름 등 일반 텍스트
int 숫자/음수 부호 외 제거 후 int 변환 게시글 ID, 페이지 번호
float 숫자/소수점/음수 부호 외 제거 금액, 좌표
slug 영문자/숫자/밑줄/하이픈 외 제거 URL 슬러그, 게시판 키
filename 경로 특수문자 제거, 연속 점(..) 정규화 업로드 파일명
url FILTER_SANITIZE_URL 적용 URL 입력 필드
email FILTER_SANITIZE_EMAIL 적용 이메일 입력 필드
html strip_tags() — HTML 태그 제거 관리자 메모 등 태그 제거 필요 시
 
// 사용 예시
$title     = Secure::sanitize($_POST["title"]);          // 기본 text
$postId    = Secure::sanitize($_GET["id"], "int");       // 정수
$boardKey  = Secure::sanitize($_GET["board"], "slug");   // 슬러그
$email     = Secure::sanitize($_POST["email"], "email"); // 이메일
$fileName  = Secure::sanitize($name, "filename");        // 파일명

// 헬퍼 함수
$title = DxSanitizer::text($_POST["title"]);    // sanitize("text") 동일
$id    = DxSanitizer::int($_GET["id"]);          // sanitize("int") 동일


9.2 esc() — HTML 출력 이스케이프

// HTML 출력 시 반드시 사용
echo Secure::esc($userInput);
// = htmlspecialchars($str, ENT_QUOTES, "UTF-8")

// 변환 대상
&  → &amp;
<  → &lt;
>  → &gt;
"  → &quot;
'  → &#039;

// 헬퍼 함수
echo dx_esc($value);

// 잘못된 패턴 (XSS 취약)
echo $_GET["name"];                    // 위험!
echo htmlspecialchars($_GET["name"]);  // flags 누락 위험

// 올바른 패턴
echo Secure::esc($_GET["name"]);       // 안전
echo dx_esc($_GET["name"]);            // 안전


9.3 safeUrl() — URL 안전 검증

// URL 출력 시 blocked: 등 위험 스킴 차단
echo Secure::safeUrl($url);

// 처리 결과
"blocked:alert(1)"  → "#"  (차단)
"data:text/html,..."   → "#"  (차단)
"blocked:..."         → "#"  (차단)
"blob:..."             → "#"  (차단)
"https://example.com"  → 그대로 (허용)
"/"                    → 그대로 (허용, 상대 경로)
"#section"             → 그대로 (허용, 앵커)
"tel:010-0000-0000"    → 그대로 (허용)
"mailto:a@b.com"       → 그대로 (허용)

// 사용 예
<a href="<?php echo Secure::safeUrl($user["website"]); ?>">홈페이지</a>


10장. 봇 탐지 (detectSuspiciousBot)

자동화 도구와 스크래퍼를 탐지합니다. 검색엔진 봇은 허용하고, 의심 봇은 로그에만 기록합니다. 차단하지 않는 이유는 실제 Googlebot이 차단될 위험을 방지하기 위해서입니다.


10.1 허용 봇 화이트리스트

// 아래 봇은 탐지 로직을 완전히 스킵
'Googlebot',         // Google 검색엔진
'Yeti',             // 네이버 검색엔진
'bingbot',          // Bing 검색엔진
'DuckDuckBot',      // DuckDuckGo 검색엔진
'Baiduspider',      // Baidu 검색엔진
'kakaotalk-scrap',  // 카카오톡 링크 미리보기
'facebookexternalhit', // Facebook 링크 미리보기
'Twitterbot',       // Twitter 링크 미리보기
'LinkedInBot',      // LinkedIn 링크 미리보기
'AhrefsBot',        // Ahrefs SEO 크롤러
'SemrushBot',       // Semrush SEO 크롤러


10.2 의심 봇 탐지 패턴

// User-Agent에서 아래 패턴 감지 시 경고 로그
// (차단 아님 — 로그만 기록)
'curl'       // curl 명령어
'wget'       // wget 다운로더
'python'     // Python requests 등
'scrapy'     // Python 스크래핑 라이브러리
'headless'   // Headless Chrome/Firefox
'selenium'   // 자동화 테스트 도구
'phantomjs'  // PhantomJS 헤드리스 브라우저

// User-Agent 비어있으면 경고 로그
// Empty User-Agent → 일반 브라우저는 UA가 비어있지 않음

// 로그 위치: data/security.log
// [2026-05-11 00:00:00][WARN][IP:1.2.3.4][UA:python-requests/2.28] Suspicious UA: ...


11장. 보안 로그


11.1 security.log 형식

// 저장 위치: data/security.log
// 형식: [날짜시간][타입][IP:주소][UA:에이전트] 메시지

// 실제 예시
[2026-05-11 14:32:11][WAF][IP:1.2.3.4][UA:Mozilla/5.0...] Blocked: sql | URI: /free/write
[2026-05-11 14:32:12][BLOCK][IP:1.2.3.4][UA:Mozilla/5.0...] WAF_SQL
[2026-05-11 15:00:01][WARN][IP:5.6.7.8][UA:python-requests/2.28] Suspicious UA: python...
[2026-05-11 16:00:00][WARN][IP:9.10.11.12][-] Empty User-Agent


11.2 로그 타입별 의미

타입 의미 및 조치
WAF SQL/XSS/LFI/CMD 공격 패턴 탐지. 동시에 IP가 30분 차단됨. 반복 발생 시 해당 경로 검토 필요
BLOCK IP 차단 등록 완료. WAF 직후 발생. Redis에 dx:ban:{IP} 키 존재
WARN 의심 봇 감지 또는 빈 User-Agent. 차단 아님. 비정상 트래픽 모니터링용


11.3 보안 로그 관리

// 로그 파일 크기 확인 및 정리 (서버 cron 예시)
// data/security.log 가 일정 크기 초과 시 자동 압축

// extend/bottom/ 에서 주기적 로그 정리
// extend/bottom/02_security_log_rotate.php
if (!defined("DX_CMS")) exit;
$logFile = DX_ROOT . "/data/security.log";
if (file_exists($logFile) && filesize($logFile) > 10485760) { // 10MB
    rename($logFile, $logFile . "." . date("Ymd"));
}


12장. Redis 연결 설정

Redis는 Rate Limit, IP 차단, 세션 저장에 사용됩니다. 선택 사항이지만 고트래픽 환경에서 강력히 권장합니다.


12.1 Redis 설정 방법

// data/config.php 에 추가

// Redis 기본 연결
define('REDIS_HOST', '127.0.0.1');  // Redis 서버 IP
define('REDIS_PORT', 6379);          // Redis 포트
define('REDIS_AUTH', '비밀번호');    // 인증 없으면 빈 문자열

// Redis 세션 저장소 (세션도 Redis에 저장할 경우)
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379');
// 비밀번호 사용 시
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379?auth=비밀번호');

// 연결 확인
$redis = Secure::getRedis();
if ($redis) {
    echo "Redis 연결 성공";
    echo "버전: " . $redis->info()["redis_version"];
} else {
    echo "Redis 없음 — 파일 폴백 사용";
}


12.2 Redis 없이도 동작하는 기능

기능 Redis 없을 때 동작
Rate Limit 파일 기반 폴백 (data/cache/rl_*.tmp). 동시성은 약간 저하
IP 차단 동작 안 함 (로그만 기록). .htaccess로 대안 가능
세션 저장 data/sessions/ 파일 세션으로 폴백. 단일 서버에서는 문제 없음
DxCache 파일 캐시로 폴백 (data/cache/*.cache.php). 성능 저하 있음


13장. 사용방법 및 보안 체크리스트


13.1 플러그인/테마에서 보안 API 사용

// 1. 입력값 정제
$title   = DxSanitizer::text(dx_post("title", ""));
$page    = max(1, (int)dx_get("p", 1));
$boardKey = Secure::sanitize(dx_get("board", ""), "slug");

// 2. CSRF 검증 (POST 처리 시 필수)
dx_csrf_check();

// 3. 로그인 여부 확인
if (!dx_is_login()) {
    dx_redirect(dx_base_url("auth/login") . "?redirect=" . urlencode(dx_current_url()));
}

// 4. Rate Limit 적용
if (!Secure::getInstance()->rateLimit("my_api", 30, 60)) {
    dx_json(array("success"=>false, "message"=>"요청이 너무 많습니다."), 429);
}

// 5. HTML 출력 이스케이프
echo Secure::esc($userData["name"]);
echo dx_esc($post["title"]);

// 6. URL 안전 출력
<a href="<?php echo Secure::safeUrl($user["website"]); ?>">

// 7. 파일 업로드 검증
$result = Secure::validateUpload($_FILES["file"]["tmp_name"], $_FILES["file"]["name"], 5242880);
if (!$result["ok"]) die($result["error"]);


13.2 보안 체크리스트

  • [ ] data/config.php의 DX_SECRET_KEY가 64자 이상 랜덤 문자열로 설정되어 있는지 확인
  • [ ] HTTPS 환경: SSL 인증서 설치 및 site_url이 https://로 시작하는지 확인
  • [ ] data/ 폴더 직접 접근 차단 (.htaccess 또는 Nginx location 설정)
  • [ ] data/security.log 파일 크기 모니터링 (크면 공격 시도 있음)
  • [ ] Redis 설치 시 비밀번호(AUTH) 설정 및 외부 접근 차단 (bind 127.0.0.1)
  • [ ] PHP 버전 7.4 이상 사용 (PHP 5.6 지원은 하지만 보안상 최신 권장)
  • [ ] 관리자 비밀번호 초기값(admin1234) 즉시 변경
  • [ ] 불필요한 플러그인 비활성화 또는 삭제
  • [ ] data/sessions/ 폴더 권한 700으로 설정 (chmod 700 data/sessions)
  • [ ] 파일 업로드 폴더(data/uploads/)에 PHP 실행 차단


13.3 공통 보안 실수 패턴

// ❌ 잘못된 패턴
echo $_GET["name"];                    // XSS 취약
$id = $_GET["id"];                     // SQL 인젝션 위험
header("Location: " . $_GET["url"]);  // 오픈 리다이렉트
$file = fopen($_POST["path"], "r");   // 경로 탐색 공격

// ✅ 올바른 패턴
echo Secure::esc($_GET["name"]);
$id = (int)$_GET["id"];
$url = Secure::safeUrl($_GET["url"]); header("Location: " . $url);
$file = fopen(DX_ROOT."/data/".basename($_POST["path"]), "r");

// ❌ CSRF 없는 폼
<form method="post"><input name="action" value="delete"></form>

// ✅ CSRF 있는 폼
<form method="post">
    <?php echo dx_csrf_field(); ?>
    <input name="action" value="delete">
</form>
 
 

댓글0

로그인 후 댓글을 작성할 수 있습니다.
13. 보안 기본 보안 구조 2026.05.10
31
전체 회원
503
전체 게시글
775
전체 댓글
442
오늘 방문
33,174
전체 방문
3
현재 접속
인기글 7일 이내
최신글
최신댓글
목록