보통 SQL 인젝션을 막기 위한 가장 효과적인 방법 중 하나로 Prepared Statement가 널리 알려져 있습니다. 저 역시 이렇게 알고 있었지만, 모든 것을 막을 순 없었습니다. 쿼리 구조를 동적으로 변경해야 하는 특정 구문에는 적용할 수 없다는 한계가 있습니다. 특히, ORDER BY 절이 그 대표적인 예시인데요.
이번 글에서는 Prepared Statement로 막을 수 없는 ORDER BY 절 SQL 인젝션의 원인과 안전한 방어 대책에 대해서 작성해 보려고 합니다.
1. 취약 원인

ORDER BY 절은 정렬 기준 컬럼(식별자)을 지정하는 구문입니다. 이 컬럼명은 실행 시점에 동적으로 결정되므로, Prepared Statement(이하 PS)의 *리터럴 바인딩 방식을 적용할 수 없습니다. PS는 미리 컴파일된 쿼리에 값만 바인딩하므로, 컬럼명처럼 쿼리 구조에 영향을 주는 부분에는 적용이 불가능합니다.
-- Prepared Statement가 적용되지 않는 예시
SELECT * FROM board ORDER BY {user_input};
위와 같이 사용자의 입력값이 컬럼명 자리에 직접 들어가게 되면, 공격자는 이 부분을 이용해 쿼리 구조를 변조하여 SQL 인젝션을 시도할 수 있습니다. 예를 들어, user_input에 title; DROP TABLE users; 와 같은 악성 쿼리를 삽입하면 데이터베이스에 심각한 피해를 줄 수 있습니다.
*리터럴 바인딩 방식
Prepared Statement에서 SQL 쿼리의 값(Value) 부분을 동적으로 전달받는 방식입니다. 이 방식은 쿼리 구조 자체는 고정시키고, 물음표(?)와 같은 플레이스홀더를 사용하여 입력될 데이터만 분리합니다.
예를 들어, SELECT * FROM users WHERE id = ?; 와 같은 쿼리를 미리 준비해두면, 이후 ? 부분에 '1' 또는 '2'와 같은 값을 바인딩하여 쿼리를 실행합니다. 이 과정에서 데이터베이스는 입력 값을 순수한 데이터로만 인식하기 때문에, 입력값에 SQL 명령어가 포함되어도 쿼리 구조를 변경할 수 없어 SQL 인젝션 공격을 방어할 수 있습니다.
2. 대응 방안
Prepared Statement를 사용할 수 없는 경우, 가장 안전한 대응책은 바로 화이트리스트를 기반으로 사용자의 입력을 검증하는 것입니다. 화이트리스트는 허용 가능한 값들의 목록을 미리 정의하고, 그 목록에 없는 값은 모두 차단하는 방식입니다.
// 예시: 화이트리스트 기반 매핑 테이블
Map<String, String> sortMap = Map.of(
"date", "created_at",
"title", "title",
"views", "view_count"
);
// 사용자의 입력 값을 안전한 컬럼명으로 변환
String sortColumn = sortMap.getOrDefault(userInput, "created_at");
// 변환된 안전한 컬럼명을 쿼리에 적용
String query = "SELECT * FROM board ORDER BY " + sortColumn;
위 코드처러 사용자가 'date', 'title', 'views' 중 하나를 선택하면, 각각 미리 정해놓은 'created_at', 'title', 'view_count' 같은 안전한 컬럼명으로 변환됩니다.
만약, 허용되지 않은 입력(ex. DROP TABLE users)이 들어오면 getOrDefault() 함수에 의해 기본값인 'created_at'으로 처리되어 SQL 인젝션 공격을 방어할 수 있습니다.
'Security > Application' 카테고리의 다른 글
| [모의해킹] 웹 애플리케이션 정보 수집 방법 (0) | 2025.03.06 |
|---|---|
| Metasploitable2 - FTP 취약점 공격 (vsftpd 2.3.4, CVE-2011-2523) (0) | 2025.02.25 |
| BeEF 활용 악성코드 공격(2) (0) | 2025.02.18 |
| BeEF 활용 악성코드 공격(1) (0) | 2025.02.18 |
