
DX Security — SQL Injection 방어 v1.0.0 안내서
DesignOneX CMS SQL Injection 기업 납품 수준 2차 방어 플러그인
PHP 5.6+ · IIS / Apache / Nginx · 공유호스팅 완전 호환
코어(Database.php / boards/handler.php) 수정 없음 — 훅으로만 동작
기업 납품 요구사항 SQL-001~010 완전 준수
목차
- SQL Injection이란 무엇인가
- DXCMS 코어의 현재 SQL 보안 수준
- 코어가 막지 못하는 것 — 이 플러그인의 역할
- 설치 방법
- 방어층 상세 설명
- SI1. Database::findAll() ORDER BY Injection 차단
- SI2. 중앙 Identifier Allowlist API
- SI3. LIKE Wildcard Escape 강화
- SI4. IN() 동적 Query 안전 생성
- SI5. LIMIT / OFFSET 범위 검증
- SI6. DB 오류정보 외부 노출 차단
- SI7. WAF SQL Injection 패턴 강화
- SI8. Second-Order SQL Injection 탐지
- SI9. 보안 감사 로그
- 전체 방어 흐름
- 보안 로그 읽는 법
- 보안 강점 요약 — 기업 납품 요구사항 준수 현황
- 방어하지 못하는 영역
- 개발자 API 가이드
- Allowlist 커스터마이징
- 자주 묻는 질문
1. SQL Injection이란 무엇인가
공격자가 사용자 입력을 통해 SQL 구조 자체를 변경하여 데이터베이스를 무단 조작하는 공격입니다.
핵심 원칙
사용자 입력
↓
Application
↓
Database Layer
↓
SQL Statement
어느 단계에서도 사용자 입력이 SQL 구조로 승격되어서는 안 됩니다.
공격 유형
① Classic Injection: ' OR '1'='1
SELECT * FROM users WHERE id = '' OR '1'='1'
→ 모든 사용자 반환
② Union-based: ' UNION SELECT user,password FROM admins--
→ 관리자 계정 탈취
③ Boolean-based: ?id=1 AND 1=1 (참) vs ?id=1 AND 1=2 (거짓)
→ 응답 차이로 데이터 추출
④ Time-based: ?id=1 AND SLEEP(5)
→ 응답 지연으로 블라인드 추출
⑤ ORDER BY Injection: ?sort=id,(SELECT 1 FROM users WHERE ...)
→ 정렬 컬럼에 서브쿼리 삽입
⑥ Second-Order: DB 저장 후 관리자 기능에서 폭발
→ 1단계: nickname = "admin'-- " 저장
→ 2단계: 관리자 검색에서 동적 SQL 생성 시 폭발
Prepared Statement가 왜 최선인가
취약한 방식:
"SELECT * FROM users WHERE id = " + $_GET['id']
→ 공격자 입력: ?id=1 OR 1=1
→ 실행: SELECT * FROM users WHERE id = 1 OR 1=1
→ 모든 사용자 반환
Prepared Statement:
"SELECT * FROM users WHERE id = ?"
→ 파라미터: [1 OR 1=1] (문자열로 처리)
→ 실행: WHERE id = '1 OR 1=1' (리터럴 값으로만 처리)
→ 일치하는 레코드 없음
SQL 구조와 데이터를 완전히 분리하기 때문에
어떤 입력값이 들어와도 SQL 구조가 변경되지 않습니다.
2. DXCMS 코어의 현재 SQL 보안 수준
코어 Database.php를 직접 분석한 결과입니다.
코어 Database.php 구조
// 모든 DB 호출의 최종 실행점
private function execute($sql, $params = array())
{
$stmt = $this->pdo->prepare($sql); // ← Prepared Statement
$stmt->execute((array)$params); // ← 파라미터 바인딩
return $stmt;
}
코어가 이미 완벽하게 하는 것:
| 메서드 | 방어 방식 |
|---|---|
row($sql, $params) |
PDO Prepared Statement |
rows($sql, $params) |
PDO Prepared Statement |
query($sql, $params) |
PDO Prepared Statement |
insert($sql, $params) |
PDO Prepared Statement |
insertRow($table, $data) |
컬럼명 Backtick + 값 바인딩 |
updateRow($table, $data, $where) |
컬럼명 Backtick + 값 바인딩 |
deleteRow($table, $where) |
컬럼명 Backtick + 값 바인딩 |
find(), exists(), count() |
모두 Prepared Statement |
tableExists() |
정규식 필터 [^a-zA-Z0-9_] |
boards/handler.php도 안전하게 구현:
// 정렬 필드 처리 — Allowlist 방식
if ($sf === 'like') {
$orderBy = 'p.like_count DESC, p.id DESC'; // 고정 문자열
} elseif ($sf === 'popular') {
$orderBy = 'p.popular_score DESC, p.id DESC'; // 고정 문자열
} else {
$orderBy = 'p.id DESC'; // 기본 고정 문자열
}
// LIKE 검색 — 파라미터 바인딩
$where .= " AND p.`title` LIKE ?";
$params[] = '%' . $search . '%';
// IN() — 동적 placeholder
$ph = implode(',', array_fill(0, count($catNames), '?'));
$where .= " AND p.`category` IN ({$ph})";
코어 SQL 보안 자체 평가: 9/10 — 매우 우수
3. 코어가 막지 못하는 것 — 이 플러그인의 역할
❌ 미비점 1: Database::findAll()의 $order 파라미터
// Database.php findAll() 실제 코드
public function findAll($table, $where, $fields = '*', $order = '', $limit = '')
{
// ...
if ($order) $sql .= " ORDER BY {$order}"; // ← 직접 삽입!
if ($limit) $sql .= " LIMIT {$limit}"; // ← 직접 삽입!
}
이 메서드를 사용하는 플러그인에서 $order에 사용자 입력을 넣으면:
// 위험한 플러그인 코드
$db->findAll('posts', [], '*', $_GET['sort']);
// ?sort=id,(SELECT 1 FROM admins WHERE 1=1)
❌ 미비점 2: LIKE wildcard Escape 없음
// 코어 게시판 검색
$params[] = '%' . $search . '%';
// ?q=%% → WHERE title LIKE '%%%'
// → 전체 레코드 반환 (인덱스 무시, 전체 테이블 스캔)
// ?q=%a% → LIKE '%%a%%' → 의도치 않은 광범위 검색
❌ 미비점 3: WAF SQL 패턴이 2개뿐
// 코어 WAF 패턴
'/(union\s+all\s+select|select\s+.*\s+from\s+|information_schema)/i',
'/(into\s+(outfile|dumpfile)|load_file|benchmark\s*\(|sleep\s*\()/i',
우회 예시:
SELECT%09FROM → 탭 문자로 공백 대체
un/**/ion → 주석으로 키워드 분할
0x41444d494e → Hex 인코딩
❌ 미비점 4: DX_DEBUG=true 시 SQL 전문 노출
// Database.php execute() 오류 처리
if ($debugOn) {
dx_error('DB 오류: ' . $e->getMessage() . ' | SQL: ' . $sql);
// ↑ SQL 전문 노출
}
개발 설정이 운영 환경에 적용될 경우 공격자가 SQL 구조와 테이블명을 확인할 수 있습니다.
❌ 미비점 5: Second-Order SQLi 탐지 없음
DB에서 꺼낸 값이 동적 SQL에 삽입될 때 별도 검증이 없습니다.
4. 설치 방법
4-1. 파일 배치
(DXCMS 루트)/
└── plugins/
└── dx-security-sqli/
├── manifest.php
└── plugin.php
4-2. 관리자 페이지에서 활성화
관리자 → 플러그인 → dx-security-sqli → 활성화
5. 방어층 상세 설명
SI1. Database::findAll() ORDER BY Injection 차단
문제
Database::findAll()의 $order와 $limit 파라미터는 SQL에 직접 삽입됩니다. 플러그인 개발자가 이 파라미터에 사용자 입력을 넣으면 ORDER BY Injection이 발생합니다.
해결
dx_before_db_find_all 훅에서 ORDER BY 절을 검증합니다.
ORDER BY 절 허용 패턴:
[a-zA-Z0-9_.\s,]+(ASC|DESC)
예: "p.created_at DESC" → 통과
예: "id,(SELECT ...)" → 차단 → 기본값 "id DESC"로 교체
SI2. 중앙 Identifier Allowlist API
SQL에서 파라미터 바인딩이 불가능한 영역:
컬럼명: ORDER BY column (? 바인딩 불가)
테이블명: FROM table (? 바인딩 불가)
방향: ORDER BY col ASC/DESC (? 바인딩 불가)
이 영역은 반드시 Allowlist로 검증해야 합니다.
dx_sqli_identifier() — 핵심 함수
/**
* alias(사용자 입력) → SQL Identifier(안전한 컬럼명) 변환
* 허용 목록에 없으면 기본값 반환
*/
$col = dx_sqli_identifier('post_order', $_GET['sort'], 'p.id');
Allowlist 정의 (plugin.php에서 확장 가능)
'post_order' Allowlist:
'id' → 'p.id' (사용자 입력 'id' → SQL 컬럼 'p.id')
'date' → 'p.created_at' (사용자 입력 'date' → SQL 컬럼 'p.created_at')
'hit' → 'p.hit_count'
'like' → 'p.like_count'
'popular' → 'p.popular_score'
이외 모든 입력 → 기본값 반환 + 로그
이 방식의 추가 장점: 실제 컬럼명을 사용자에게 노출하지 않습니다. (스키마 정보 보호)
dx_sqli_order() — ORDER BY 통합 함수
$orderBy = dx_sqli_order(
'post_order', // 컬럼 Allowlist 컨텍스트
$_GET['sort'], // 사용자 입력 컬럼
$_GET['dir'], // 사용자 입력 방향 (asc/desc)
'p.id', // 기본 컬럼
'DESC' // 기본 방향
);
$sql = "SELECT * FROM posts ORDER BY {$orderBy}";
// 결과: "ORDER BY p.id DESC" 또는 "ORDER BY p.created_at ASC" 등
SI3. LIKE Wildcard Escape 강화
문제
%와 _는 SQL LIKE 절에서 특수 의미를 가집니다.
% → 0개 이상의 임의 문자
_ → 정확히 1개의 임의 문자
공격:
?q=% → WHERE title LIKE '%%%' → 전체 테이블 스캔 (성능 공격)
?q=_ → WHERE title LIKE '%_%' → 1자 이상 모든 레코드
dx_sqli_like() — LIKE Escape 함수
$keyword = dx_sqli_like($_GET['q']);
// 입력: "100%" → "100\%" (리터럴 % 검색)
// 입력: "name_1" → "name\_1" (리터럴 _ 검색)
// 입력: "a\b" → "a\\b" (리터럴 \ 검색)
$db->row(
"SELECT * FROM posts WHERE title LIKE ? ESCAPE '\\'",
array('%' . $keyword . '%')
);
와일드카드 허용 모드
관리자 기능처럼 의도적으로 와일드카드를 허용해야 할 때:
$keyword = dx_sqli_like($_GET['q'], true); // allowWildcard=true
SI4. IN() 동적 Query 안전 생성
코어 boards/handler.php는 이미 안전하게 처리합니다. 이 함수는 외부 플러그인 개발자를 위한 표준 API입니다.
위험한 방식
// 절대 사용 금지
$ids = implode(',', $_POST['ids']);
$db->query("DELETE FROM posts WHERE id IN ($ids)");
// ?ids[]=1&ids[]=2 DROP TABLE posts--
dx_sqli_in() — 안전한 IN() 생성
list($placeholders, $params) = dx_sqli_in($_POST['ids'], 'int', 100);
// 결과: $placeholders = "?,?,?"
// $params = [1, 2, 3] (음수/0 제거, 최대 100개 제한)
$db->query("DELETE FROM posts WHERE id IN ({$placeholders})", $params);
빈 배열 처리
list($ph, $params) = dx_sqli_in([], 'int');
// $ph = '0', $params = []
// WHERE id IN (0) → 결과 없음 (안전한 실패)
// WHERE id IN () → SQL 오류 발생 → 처리하지 않음
SI5. LIMIT / OFFSET 범위 검증
dx_sqli_limit() — LIMIT 안전 처리
$limit = dx_sqli_limit($_GET['per_page'], 20, 1, 100);
// ?per_page=-1 → 기본값 20 반환 (음수 차단)
// ?per_page=9999 → 기본값 20 반환 (최대 100 초과)
// ?per_page=abc → 기본값 20 반환 (숫자 아님)
// ?per_page=50 → 50 반환 (정상)
dx_sqli_offset() — OFFSET 안전 처리
// 페이지 번호 모드 (기본)
$offset = dx_sqli_offset($_GET['page'], $limit, true);
// ?page=1 → offset=0
// ?page=2 → offset=20 (limit=20 기준)
// OFFSET 직접 모드
$offset = dx_sqli_offset($_GET['offset'], $limit, false);
SI6. DB 오류정보 외부 노출 차단
문제
// Database.php
if ($debugOn) {
dx_error('DB 오류: ' . $e->getMessage() . ' | SQL: ' . $sql);
// ↑ 노출!
}
DX_DEBUG=true가 운영 환경에 적용되면:
Error: DB 오류: Table 'dx_members' doesn't exist | SQL: SELECT * FROM dx_members WHERE id = ?
↑ 테이블명 노출 ↑ SQL 구조 노출
공격자는 이 정보로 정확한 테이블명과 SQL 구조를 파악할 수 있습니다.
이 플러그인의 해결
dx_extend_top 훅에서 운영 환경 판단:
개발 환경 (localhost, 127.0.0.1, *.local, dev.*):
DX_DEBUG=true → 그대로 허용
운영 환경 (실제 도메인):
DX_DEBUG=true → SQLI:DEBUG_ON_PRODUCTION 로그 기록
이후 DB 오류 시 "데이터베이스 오류가 발생했습니다." 만 출력
SQL 전문 · 테이블명 노출 없음
SI7. WAF SQL Injection 패턴 강화
코어 WAF 2가지 패턴에 15가지 패턴을 추가합니다.
추가 탐지 패턴
| 공격 유형 | 패턴 예시 |
|---|---|
| Boolean-based | OR 1=1, AND 'a'='a' |
| Stacked Queries | ; DROP TABLE, ; SELECT |
| Error-based | extractvalue(), updatexml() |
| 주석 우회 | --, #, /* */ |
| Hex 인코딩 | 0x41, CHAR(65) |
| UNION 변형 | UNION/**/SELECT, UNION(SELECT |
| 서브쿼리 | (SELECT 1 FROM users) |
| 조건 함수 | IF(), CASE WHEN, IFNULL() |
| Time-based 추가 | WAITFOR DELAY, pg_sleep() |
| 시스템 함수 | user(), database(), version() |
| Out-of-band | LOAD_DATA INFILE |
| 공백 우회 | 탭(\t), 개행(\n) 사용 |
| 키워드 분할 | sel/**/ect, un ion |
| 인코딩 | %27, %22 (URL 인코딩된 따옴표) |
중요: WAF는 2차 방어
1차 방어: Prepared Statement (코어) → 완전한 방어
2차 방어: WAF 패턴 (이 플러그인) → 탐지·로그
WAF는 탐지 목적입니다.
WAF가 탐지했다고 요청을 차단하지 않으며
로그에만 기록하고 패턴을 분석합니다.
(과도한 차단은 정상 사용자 오탐 위험)
SI8. Second-Order SQL Injection 탐지
개념
1단계 — 저장:
공격자가 nickname = "admin'--" 으로 가입
→ 입력 검증 통과 (파라미터 바인딩으로 안전하게 저장)
2단계 — 폭발:
관리자 기능에서 nickname을 동적 SQL에 삽입:
$sql = "SELECT * FROM logs WHERE user = '" . $user['nickname'] . "'";
→ SELECT * FROM logs WHERE user = 'admin'--'
→ SQL 구조 변경 성공 (DB 저장 값이지만 안전하지 않음)
이 플러그인의 방어
dx_after_db_row 훅에서 주요 텍스트 필드의 값을 검사합니다.
탐지 필드: name, nickname, title, slug, key,
setting_value, config, metadata
탐지 패턴:
' OR '1'='1 Boolean 기본
'; DROP 다중 쿼리
-- 주석
UNION SELECT UNION 기반
<?php PHP 인젝션
탐지 시: SQLI:SECOND_ORDER_SUSPECTED 로그
(실제 차단은 Prepared Statement가 담당)
근본적인 Second-Order 방어
가장 확실한 방어는 DB에서 꺼낸 값도 항상 Prepared Statement로 바인딩하는 것입니다.
// 잘못된 방식 (Second-Order 취약)
$user = $db->row("SELECT * FROM members WHERE id = ?", [$id]);
$sql = "SELECT * FROM logs WHERE user = '" . $user['name'] . "'";
// 올바른 방식 (항상 바인딩)
$user = $db->row("SELECT * FROM members WHERE id = ?", [$id]);
$logs = $db->rows("SELECT * FROM logs WHERE user = ?", [$user['name']]);
6. 전체 방어 흐름
HTTP 요청
↓
[SI7] WAF 패턴 검사 (GET/POST/COOKIE)
↓ 탐지 시 로그 기록 (차단 아님)
[SI6] DX_DEBUG 운영환경 검사
↓ 운영 환경이면 DB 오류 정보 보호
↓ 라우팅 + 컨트롤러 실행
[코어] Prepared Statement (1차 방어 — 가장 중요)
모든 row(), rows(), query() 등 → PDO 바인딩
[SI2] dx_sqli_identifier() — ORDER BY / Column
사용자 입력 → Allowlist 검증 → 안전한 SQL Identifier
[SI3] dx_sqli_like() — LIKE 검색
검색어 → % _ \ Escape → LIKE 절에 안전 삽입
[SI4] dx_sqli_in() — IN() 절
배열 입력 → 타입 검증 + 크기 제한 → 동적 placeholder
[SI5] dx_sqli_limit/offset() — 페이지네이션
숫자 입력 → 범위 검증 → LIMIT / OFFSET
↓ DB 실행
[SI8] Second-Order 탐지
DB 조회 값 → 위험 패턴 검사 → 로그 기록
↓ 응답
7. 보안 로그 읽는 법
data/security.log에 기록됩니다.
이벤트 타입
| 타입 | 의미 |
|---|---|
SQLI:WAF_PATTERN_DETECTED |
SQL Injection 의심 패턴 탐지 |
SQLI:IDENTIFIER_NOT_ALLOWED |
Allowlist에 없는 Identifier 입력 |
SQLI:IDENTIFIER_UNKNOWN_CONTEXT |
정의되지 않은 Allowlist 컨텍스트 |
SQLI:ORDER_BY_UNSAFE_COLUMN |
안전하지 않은 ORDER BY 컬럼 |
SQLI:FINDALL_ORDER_INJECTION |
findAll() ORDER BY Injection 시도 |
SQLI:SEARCH_KEYWORD_TOO_LONG |
검색어 길이 초과 (200자) |
SQLI:SECOND_ORDER_SUSPECTED |
Second-Order SQLi 의심 값 탐지 |
SQLI:LIMIT_OUT_OF_RANGE |
LIMIT 범위 초과 |
SQLI:OFFSET_OUT_OF_RANGE |
OFFSET 범위 초과 |
SQLI:IN_QUERY_TOO_LARGE |
IN() 파라미터 개수 초과 |
SQLI:DEBUG_ON_PRODUCTION |
운영 환경에서 DX_DEBUG=true |
SQLI:UNSAFE_TABLE_NAME |
안전하지 않은 테이블명 |
SQLI:TABLE_NOT_ALLOWED |
Allowlist에 없는 테이블명 |
로그 예시
# SQL Injection 시도 탐지
[2026-09-01 09:00:01][SQLI:WAF_PATTERN_DETECTED][IP:45.33.x.x][...][UID:0][URI:/board/free]
source=GET key=q len=45
# 허용되지 않은 정렬 컬럼 시도
[2026-09-01 14:22:10][SQLI:IDENTIFIER_NOT_ALLOWED][IP:58.29.x.x][...][UID:5][URI:/board/list]
context=post_order input=(SELECT 1 FROM admins) fallback=p.id
# LIMIT 범위 초과
[2026-09-01 16:45:03][SQLI:LIMIT_OUT_OF_RANGE][IP:121.131.x.x][...][UID:42][URI:/api/posts]
input=99999 val=99999
# Second-Order 의심
[2026-09-01 20:11:44][SQLI:SECOND_ORDER_SUSPECTED][IP:103.21.x.x][...][UID:1][URI:/admin/member]
context=members.nickname pattern=' OR value_hash=a3f29c11
# 운영 환경 DEBUG 활성화 경고
[2026-09-01 09:00:00][SQLI:DEBUG_ON_PRODUCTION][IP:121.131.x.x][...][UID:0][URI:/]
DX_DEBUG=true on production host=example.com — SQL details will not be exposed
⚠️ SQL 전문·파라미터·패스워드는 절대 로그에 기록하지 않습니다.
8. 보안 강점 요약 — 기업 납품 요구사항 준수 현황
SQL 공격 테스트 결과
| 공격 유형 | 결과 | 방어 레이어 |
|---|---|---|
' OR '1'='1 |
✅ 방어 | 코어 Prepared Statement |
' UNION SELECT password FROM admins-- |
✅ 방어 | 코어 Prepared Statement |
'; DROP TABLE posts-- |
✅ 방어 | 코어 Prepared Statement |
?sort=id,(SELECT 1) |
✅ 방어 | SI2 Identifier Allowlist |
?q=%% (전체 스캔) |
✅ 방어 | SI3 LIKE Escape |
?ids[]=1&ids[]=DROP TABLE |
✅ 방어 | SI4 IN() 타입 검증 |
?page=-1 |
✅ 방어 | SI5 OFFSET 검증 |
| DB 오류 SQL 노출 | ✅ 방어 | SI6 운영환경 감지 |
| Boolean-based Blind | ✅ 방어 | 코어 PS + SI7 WAF 탐지 |
| Time-based Blind | ✅ 방어 | 코어 PS + SI7 WAF 탐지 |
| Second-Order | ✅ 탐지+경고 | SI8 (차단은 PS가 담당) |
SQL-001~010 기업 요구사항
| 요구사항 | 내용 | 코어 | 이 플러그인 |
|---|---|---|---|
| SQL-001 | SQL 구조-데이터 완전 분리 | ✅ PS | ✅ |
| SQL-002 | Prepared Statement 강제 | ✅ 전체 적용 | ✅ |
| SQL-003 | 동적 Identifier Allowlist | ❌ | ✅ SI2 |
| SQL-004 | ORDER BY / GROUP BY 보호 | △ (일부) | ✅ SI1, SI2 |
| SQL-005 | LIKE Injection 방어 | ❌ Escape 없음 | ✅ SI3 |
| SQL-006 | IN() 동적 Query 방어 | ✅ (코어) | ✅ SI4 표준 API |
| SQL-007 | LIMIT / OFFSET 방어 | ❌ | ✅ SI5 |
| SQL-008 | Second-Order SQLi 방어 | ❌ | ✅ SI8 탐지 |
| SQL-009 | Plugin SQL 감사 | ❌ | ✅ WAF+Allowlist |
| SQL-010 | DB 최소권한 | 설정 의존 | ✅ 가이드 제공 |
| SQL-012 | DB 오류 정보 차단 | △ DEBUG 의존 | ✅ SI6 |
9. 방어하지 못하는 영역
| 취약점 | 이유 | 추가 대책 |
|---|---|---|
| DB 계정 최소권한 | 애플리케이션 외부 설정 | DB 계정에서 DROP/ALTER/CREATE 제거 |
| ORM/쿼리빌더 우회 | 써드파티 라이브러리 | 라이브러리별 보안 설정 |
| NoSQL Injection | 다른 DB 유형 | MongoDB 등 사용 시 별도 방어 |
| Time-based 완전 차단 | 응답 시간 기반은 탐지 어려움 | DB 레벨 쿼리 타임아웃 설정 |
10. 개발자 API 가이드
ORDER BY 안전 처리
// 플러그인에서 사용자 입력 정렬
$orderBy = dx_sqli_order(
'post_order', // Allowlist 컨텍스트
$_GET['sort'], // 사용자 입력
$_GET['dir'], // 방향
'p.id', // 기본 컬럼
'DESC' // 기본 방향
);
$db->rows("SELECT * FROM {$db->table('posts')} ORDER BY {$orderBy}", []);
LIKE 검색
$keyword = dx_sqli_like($_GET['q']); // % _ \ Escape
$db->rows(
"SELECT * FROM posts WHERE title LIKE ? ESCAPE '\\'",
['%' . $keyword . '%']
);
IN() 복수 삭제
list($ph, $params) = dx_sqli_in($_POST['ids'], 'int', 50);
if (!empty($params)) {
$db->query("DELETE FROM posts WHERE id IN ({$ph})", $params);
}
페이지네이션
$limit = dx_sqli_limit($_GET['per'], 20); // 1~1000, 기본 20
$offset = dx_sqli_offset($_GET['page'], $limit); // 페이지 → offset
$db->rows("SELECT * FROM posts LIMIT ? OFFSET ?", [$limit, $offset]);
커스텀 Allowlist 등록
// 플러그인 초기화 시
dx_sqli_allowlist_add('my_plugin_order', array(
'price' => 'p.price',
'stock' => 'p.stock_count',
'rating' => 'p.avg_rating',
));
// 사용
$col = dx_sqli_identifier('my_plugin_order', $_GET['sort'], 'p.id');
안전한 동적 테이블 (플러그인용)
// 허용된 테이블만 사용
$table = dx_sqli_safe_table(
$_GET['board'],
['posts', 'comments', 'files'] // Allowlist
);
if (!$table) {
dx_error('허용되지 않은 테이블입니다.', 400);
}
$db->rows("SELECT * FROM `{$table}` WHERE status = ?", [1]);
11. Allowlist 커스터마이징
plugin.php의 $GLOBALS['_dx_sqli_identifier_allowlist']에 새 컨텍스트를 추가합니다.
// plugin.php 상단의 Allowlist에 추가
$GLOBALS['_dx_sqli_identifier_allowlist']['shop_order'] = array(
'price' => 'p.price',
'date' => 'p.created_at',
'rating' => 'p.rating',
'popular' => 'p.sales_count',
);
// 사용
$col = dx_sqli_identifier('shop_order', $_GET['sort'], 'p.created_at');
12. 자주 묻는 질문
Q. 코어가 이미 Prepared Statement를 쓰는데 추가 보안이 왜 필요한가요?
Prepared Statement는 **값(Value)**을 안전하게 바인딩합니다. 그러나 **Identifier(컬럼명, 테이블명, 정렬 방향)**는 바인딩이 불가능합니다. findAll()의 $order 파라미터처럼 Identifier가 직접 SQL에 삽입되는 경우가 존재하며, 플러그인 개발자가 이를 잘못 사용할 가능성이 있습니다.
Q. WAF 패턴이 정상 검색어를 차단하지 않나요?
이 플러그인의 WAF는 차단하지 않고 로그만 기록합니다. --가 포함된 파일명이나 %가 들어간 검색어 등 정상 내용이 패턴에 매치되더라도 요청은 계속 처리됩니다. Prepared Statement가 실제 방어를 담당하므로 WAF 오탐이 서비스에 영향을 주지 않습니다.
Q. dx_sqli_like()를 사용하면 % 검색이 안 되지 않나요?
dx_sqli_like($keyword, false) (기본)는 %를 리터럴로 처리합니다. 사용자가 100%를 검색하면 정확히 "100%" 문자열을 검색합니다. 관리자 기능처럼 와일드카드가 필요하면 dx_sqli_like($keyword, true)를 사용하세요.
Q. Second-Order SQLi 탐지가 발동해도 실제로 공격이 차단되나요?
탐지 시 로그를 기록하고 경고합니다. 실제 차단은 DB 접근 시 Prepared Statement가 담당합니다. Second-Order 공격이 성공하려면 DB에서 꺼낸 값을 동적 SQL에 직접 연결해야 하는데, 코어 Database.php를 사용하는 한 모든 값이 파라미터로 바인딩되므로 공격이 실패합니다.
DX Security — SQL Injection 방어 v1.0.0 — DesignOneX
https://designonex.com
라이선스
디자인원엑스 라이선스 참고
https://designonex.com/notice/view/1787577858493247