SQL Injection은 웹 애플리케이션 보안에서 가장 흔하고 치명적인 취약점 중 하나.
원리부터 PreparedStatement, ORM을 통한 보호 방법까지 설명하려 함
1. SQL Injection 원리
SQL Injection은 웹 애플리케이션의 입력 폼
이나 URL 파라미터
등을 통해 악의적인 SQL 구문(Query)을 주입(Injection)하여, 데이터베이스를 비정상적으로 조작하거나 민감한 정보를 탈취하는 공격 기법
핵심 원리: 사용자 입력이 SQL 쿼리에 직접 결합
애플리케이션이 사용자로부터 입력받은 값을 아무런 검증이나 처리 없이 SQL 쿼리 문자열에 직접 삽입하여 실행할 때 발생
SELECT * FROM users WHERE username = '사용자_입력_ID' AND password = '사용자_입력_PW';
공격자는 사용자_입력_ID
부분에 SQL 예약어 (OR, AND, ', -- , etc..)
를 포함하여 주입
예시1: 인증 우회
- 공격자가 사용자 ID에
' OR 1=1 --
를 입력한다고 가정 ( -- : 주석) - 원래 쿼리:
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = '사용자_입력_PW';
- 이 쿼리는
username=' '
조건이 거짓이어도OR 1=1
부분이 항상 참이 되므로, 비밀번호를 몰라도 로그인 성공
👉--
뒤의 내용은 주석 처리되어 무시
예시2: 데이터 탈취
- 공격자가 검색 폼에
' ; DROP TABLE users' --
와 같은 쿼리를 입력한다고 가정 - 원래 쿼리:
SELECT * FROM products WHERE name LIKE '%'; DROP TABLE users; --%';
- 이러면 정상적인 쿼리 검색 후, users 테이블이 삭제되는 심각한 문제 발생
예시3: 정보 노출
- 공격자가
' UNION SELECT password, username FROM users --
와 같은 쿼리를 입력한다고 가정 - 원래 쿼리:
SELECT name FROM products WHERE category = '' UNION SELECT password, username FROM users --';
- UNION SELECT를 통해 products 테이블의 검색 결과와 함께 users 테이블의 사용자 이름과 비밀번호가 함께 반환
❓ 왜 작은 따옴표로 시작하는가
= 공격자는 먼저 애플리케이션이 원래 의도했던 문자열 리터럴을 주입된 작은따옴표로 강제로 닫아버림
PreparedStatement를 통한 보호 방법
PreparedStatement (또는 Parameterized Query, 바인딩 변수 방식)는 SQL Injection을 방어하는 가장 효과적이고 권장되는 방법
원리:
- SQL 쿼리 구조와 사용자 입력 데이터를 완전히 분리하여 처리
- SQL 쿼리는 미리 컴파일되거나 DB 전달 시 구조 고정
- 사용자 입력 데이터는 쿼리 내의
?
또는:이름
에 값으로 바인딩 됨
이 데이터는 SQL 구문이 아닌 순수한 데이터 값으로만 인식 됨
장점 | 설명 |
SQL Injection 방어 | 사용자 입력이 SQL 구문의 일부로 해석되지 않으므로, 악의적인 SQL 코드 주입 근본적으로 차단 |
성능 향상 | 쿼리 구조가 미리 정의 & 컴파일 되므로, 동일한 쿼리를 여러 번 실행할 때 오버헤드를 줄여 성능 향상 |
작동 방식
1. SQL 쿼리 정의
String sql = "SELECT * FROM users WHERE username = ? AND password = ?;";
PreparedStatement pstmt = connection.prepareStatement(sql);
?
는 플레이스홀더로, 실제 값이 들어갈 위치를 나타냄
DB는 SELECT * FROM users WHERE username = [값] AND password = [값];
형태의 쿼리 구조를 인식
2. 데이터 바인딩
pstmt.setString(1, userInputId); // 첫 번째 ? 에 사용자 입력 ID 바인딩
pstmt.setString(2, userInputPw); // 두 번째 ? 에 사용자 입력 PW 바인딩
setString()
메소드는 userInputId와 userInputPw를 문자열 데이터 값으로 처리하여 쿼리에 삽입- 이 값 안에 SQL 예약어(예:
OR 1=1 --
)가 포함되어 있더라도, 데이터베이스는 문자열로만 인식 (SQL 구문 X) - 예시: userInputId가 ' OR 1=1 -- 이더라도, 데이터베이스는
username = ''' OR 1=1 --'
(문자열 리터럴로 처리되어 단일 인용부호가 이스케이프 됨)으로 인식하여 공격을 무력화
ORM (Object- Relational Mapping) 을 통한 보호 방법
ORM은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 기술
대표적인 ORM: Java의 Hibernate/JPA, Python의 SQLAlchemy, Node.js의 Sequelize
ORM의 SQL Injection 보호 원리:
- 대부분의 최신 ORM 프레임워크는 내부적으로 PreparedStatement와 같은 파라미터 바인딩 방식을 사용
- 직접 SQL 쿼리 문자열을 조작할 필요 없이, 객체 지향적인 방식으로 DB 작업을 수행하도록 추상화 계층을 제공
장점 | |
높은 생산성 | SQL 쿼리 직접 작성X, 개발 생산성 향상 |
유지보수 용이 | 객체지향적 코드로 DB 작업을 수행하므로, 가독성 & 유지보수성 좋음 |
기본적인 SQL Injection 방어 | 기본적으로 파라미터 바인딩을 사용, 별도의 조치 없이도 SQL Injection 공격 방어 |
작동 방식
1. 객체 모델 정의
// Java (JPA/Hibernate 예시)
@Entity
public class User {
@Id
private Long id;
private String username;
private String password;
// ... getters and setters
}
2. 데이터베이스 작업 수행 (ORM API 사용):
// 사용자 ID로 User 객체 조회
User user = entityManager.createQuery("SELECT u FROM User u WHERE u.username = :username", User.class)
.setParameter("username", userInputId) // ORM이 내부적으로 파라미터 바인딩
.getSingleResult();
- 개발자는 SQL 쿼리를 직접 작성하는 대신 JPQL이나 HQL과 같은 ORM 전용 쿼리 언어를 사용하거나
메서드 호출(예: userRepository.findByUsername(userInputId))만으로 DB 작업 수행 - 이 과정에서 ORM 프레임워크가 개발자를 대신하여 안전한 PreparedStatement 기반의 SQL 쿼리 생성 & 실행
주의 사항
- 원시 SQL(Native SQL) 사용 시: ORM을 사용하더라도 때로는 복잡한 쿼리나 성능 최적화를 위해 직접 SQL 쿼리(Native Query)를 작성하여 실행해야 할 때가 존재
이 경우 반드시 PreparedStatement의 원리를 적용하여 파라미터 바인딩을 수동으로 해주어야 함.
∵ ORM의 보호를 벗어나기 때문
// ORM에서 Native Query 사용 시 주의 (JPA 예시)
Query nativeQuery = entityManager.createNativeQuery("SELECT * FROM users WHERE username = ?");
nativeQuery.setParameter(1, userInputId); // 반드시 파라미터 바인딩 사용!
- 동적 쿼리 생성 시: 개발자가 ORM의 API를 통해 동적으로 쿼리 조건을 생성할 때, 문자열 결합 방식으로 필터링 조건을 구성하면 SQL Injection 취약점이 발생할 수도 있음
항상 ORM이 제공하는 안전한 필터링/조건 추가 API를 사용
결론
SQL Injection은 사용자 입력 값을 SQL 쿼리에 직접 문자열 결합하는 방식에서 비롯됨.
이를 방어하는 가장 확실한 방법은 PreparedStatement(파라미터 바인딩)를 사용하여 쿼리 구조와 데이터 값을 분리
ORM은 이 PreparedStatement의 원리를 내부적으로 활용하여 개발자가 SQL Injection에 노출될 위험을 크게 줄여주는 훌륭한 도구지만, ORM을 사용하더라도 원시 SQL을 사용하거나 부적절하게 동적 쿼리를 생성할 때는 여전히 SQL Injection에 주의해야 함