이번 시간에는 쿼리 인젝션에 대해서 알아보자
웹 어플리케이션의 보안은 개발 과정에서 가장 중요한 고려사항 중 하나다. 특히, 데이터베이스와 상호작용하는 웹 사이트에서는 SQL 인젝션과 같은 공격이 심각한 위협이 될 수 있다. 이번 글에서는 SQL 인젝션의 개념부터 시작하여, 이러한 공격이 어떻게 이루어지는지, 그리고 이를 효과적으로 방지하기 위한 방법들에 대해 알아보도록 하자. 특히, 나는 Spring Boot를 주로 사용하기에 SpringBoot 환경에서 쿼리 인젝션을 방지하기 위한 실질적인 전략도 약간 다뤄보고자 한다.
1. 쿼리 인젝션의 위험성과 중요성
1-1. 쿼리 인젝션 이란?
쿼리 인젝션은 웹 어플리케이션 데이터베이스의 쿼리에 임의의 SQL 코드를 삽입하여 공격자가 데이터베이스를 조작할 수 있게 하는 웹 보안 취약점이다. 간단히 말해, 공격자는 사용자 입력을 조작하여 데이터베이스에 악의적인 SQL을 주입할 수 있다. 이를 통해 공격자는 정상적으로 접근할 수 없는 데이터를 열람하거나 수정, 삭제할 수 있다. 공격자는 SQL 인젝션을 이용하여 백엔드 서버 또는 기타 인프라를 손상시키거나 서비스 거부 공격을 수행할 수 있다.
예를 들어, 웹 어플리케이션에서 사용자가 로그인할 때, 시스템은 입력된 사용자 이름과 비밀번호를 데이터베이스와 비교하여 검증한다. 만약 사용자 이름 필드에 공격자가 ' OR '1'='1' 같은 SQL 코드를 입력한다면, 이 쿼리는 데이터베이스에서 모든 사용자에 대해 참이 되는 조건으로 해석될 수 있다. 결과적으로, 이러한 입력을 통해 공격자는 데이터베이스가 보유한 모든 사용자 정보에 접근할 수 있게 되는 것이다. 이는 쿼리 인젝션의 전형적인 예시로, 웹 어플리케이션의 보안 취약점을 악용하는 방법이다.
1-2. 백엔드 개발에서 쿼리 인젝션의 위험
- 데이터 무결성 위협
- 쿼리 인젝션 공격은 데이터베이스의 내용을 삭제, 복사, 수정할 수 있으므로 중요한 데이터의 무결성을 위협한다.
- 쿼리 인젝션 공격은 데이터베이스의 내용을 삭제, 복사, 수정할 수 있으므로 중요한 데이터의 무결성을 위협한다.
- 다양한 공격 경로
- 공격자는 사용자 입력, 쿠키, HTTP 헤더 등 다양한 방법을 통해 SQL 인젝션 공격을 시도할 수 있다. 특히, 쿠키나 서버 변수를 조작하여 데이터베이스 쿼리를 오염시킬 수 있다.
- 공격자는 사용자 입력, 쿠키, HTTP 헤더 등 다양한 방법을 통해 SQL 인젝션 공격을 시도할 수 있다. 특히, 쿠키나 서버 변수를 조작하여 데이터베이스 쿼리를 오염시킬 수 있다.
- 2차 쿼리 인젝션 위험
- 개발자가 즉각적인 공격에 대해 입력을 제대로 정제했더라도, 나중에 다른 상황에서 사용될 때 발생하는 '2차 쿼리 인젝션'이라는 위험도 있다.
- 개발자가 즉각적인 공격에 대해 입력을 제대로 정제했더라도, 나중에 다른 상황에서 사용될 때 발생하는 '2차 쿼리 인젝션'이라는 위험도 있다.
- 자동화된 공격 도구 사용
- SQL 인젝션 공격은 SQLninja, SQLmap, Havij와 같은 도구를 통해 자동화될 수 있다. 이러한 도구들은 보안 취약점을 빠르게 찾아내는 데 사용된다.
2. 쿼리 인젝션의 공격 예시
2-1. 데이터 검색 공격
공격자는 SQL 쿼리를 변경하여 숨겨진 데이터를 검색할 수 있다. 예를 들어, 쇼핑 어플리케이션에서 '이벤트' 카테고리에 대한 SQL 쿼리를 변경하여 아직 공개되지 않은 이벤트 제품에 대한 정보를 검색할 수 있다.
1. 쇼핑 어플리케이션에서 '이벤트' 카테고리에 대한 SQL 쿼리는 다음과 같다.
SELECT *
FROM products
WHERE category = 'Event' AND released = 1
2. 공격자가 URL에 ?category=Event'-- 를 이력해서 쿼리가 아래와 같이 변경된다.
- SQL에서--는 주석을 나타내는 표시다. 쿼리에서--뒤에 오는 모든 내용은 주석으로 처리되어 SQL 실행에 영향을 주지 않게 된다. 따라서 -뒤에 오는AND released = 1부분은 실행되지 않는다. 이 말은 공개되지 않은 (released = 0인) 제품까지 포함하여 모든 'Event' 카테고리 제품을 조회하게 된다는 의미이다.
SELECT *
FROM products
WHERE category = 'Event'-- AND released = 1
2-2. 응용 프로그램 논리 변경 공격
로그인과 같은 기능에서 공격자는 SQL 주석을 사용하여 패스워드 확인 절차를 제거함으로써 어떤 사용자로도 로그인할 수 있다.
1. 로그인 시스템의 SQL 쿼리가 아래와 같이 설정되어 있다.
SELECT *
FROM users
WHERE username = '입력된_이름'
AND password = '입력된_비밀번호'
2. 이때 공격자가 사용자 이름 필드에 admin'--을 입력하면, 이 쿼리는 아래와 같이 변경된다.
SELECT *
FROM users
WHERE username = 'admin'-- AND password = '입력된_비밀번호'
3. 이렇게 쿼리가 변경되면 -- 뒤는 주석으로 사라지게 되니까 실제 쿼리는 비밀번호 확인 절차 없이 admin 사용자로 로그인할 수 있게 된다.
SELECT *
FROM users
WHERE username = 'admin'
2-3. 다른 데이터베이스 테이블에서 데이터 검색 공격
'UNION' 키워드를 사용하여 추가적인 'SELECT' 쿼리를 실행하고 원래 쿼리 결과에 추가하여 다른 테이블의 데이터를 검색할 수 있다.
1. 만약 웹 어플리케이션이 다음과 같은 쿼리를 사용한다고 하자.
SELECT name, description
FROM products
WHERE category = '입력된_카테고리'
2. 공격자가 입력 필드에 다음과 같이 입력한다.
' UNION
SELECT username, password
FROM users--
3. 쿼리는 다음과 같이 변경된다.
SELECT name, description
FROM products
WHERE category = ''
UNION
SELECT username, password
FROM users--'
4. 이렇게 변경되면 맨 뒤의 --는 주석이므로 실제로는 아래와 같이 실행된다. 만약 이렇게 실행된다면 제품의 이름과 설명 뿐만 아니라 모든 사용자의 이름과 비밀번호도 반환하게 된다.
SELECT name, description
FROM products
WHERE category = ''
UNION
SELECT username, password
FROM users
3. 쿼리 인젝션의 작동 방식 설명
3-1. 공격 방식
다양한 입력 포인트를 통해 SQL 쿼리에 악의적인 코드를 주입한다. 이는 사용자 입력, 쿠키, 서버 변수, HTTP 헤더 등 다양한 경로를 통해 이루어질 수 있다.
- 사용자 입력: 로그인 폼의 사용자 이름 필드에 ' OR '1'='1를 입력하여 인증 과정을 우회한다.
SELECT *
FROM users
WHERE username = '' OR '1'='1';
- 쿠키 조작: 사용자의 세션 쿠키 값을 ' OR '1'='1로 변경하여 보안을 우회한다.
3-2. 공격 유형
첫 번째 순서(SQLi) 및 두 번째 순서 SQL 인젝션 공격이 존재한다. 두 번째 순서 공격은 초기 입력이 데이터베이스에 저장된 후 다른 콘텍스트에서 사용될 때 발생한다.
- 첫 번째 순서 (SQLi):
- 위의 사용자 입력 예시와 같이 직접적인 SQL 코드를 주입한다.
- 위의 사용자 입력 예시와 같이 직접적인 SQL 코드를 주입한다.
- 두 번째 순서 (SQLi):
- 두 번째 순서 SQL 인젝션은 사용자 입력이 데이터베이스에 저장된 후, 다른 쿼리에서 이를 사용할 때 발생한다. 예를 들어, 웹 사이트에서 사용자가 댓글을 작성하고 저장한 후, 이 댓글이 조회 쿼리에 사용되면, 적절히 처리되지 않은 댓글 내용이 데이터베이스 쿼리를 변경하여 의도치 않은 명령을 실행할 수 있다.
- 두 번째 순서 SQL 인젝션은 사용자 입력이 데이터베이스에 저장된 후, 다른 쿼리에서 이를 사용할 때 발생한다. 예를 들어, 웹 사이트에서 사용자가 댓글을 작성하고 저장한 후, 이 댓글이 조회 쿼리에 사용되면, 적절히 처리되지 않은 댓글 내용이 데이터베이스 쿼리를 변경하여 의도치 않은 명령을 실행할 수 있다.
3-3. 다양한 콘텍스트에서의 SQL 인젝션
쿼리 문자열 뿐만 아니라 JSON 또는 XML 형식의 입력을 통해서도 SQL 인젝션 공격을 수행할 수 있다.
- JSON/XML 입력: API 요청에서 JSON/XML 데이터에 ' OR '1'='1를 포함하여 보안을 우회한다.
{
"username": "' OR '1'='1"
}
4. Spring Boot 환경에서 쿼리 인젝션 방지 전략
4-1. Prepared Statements 사용
- SQL 인젝션을 방지하는 효과적인 방법 중 하나는 Prepared Statements를 사용하는 것이다. 이 방법은 SQL 쿼리에 사용자가 제공한 값이 들어가야 하는 부분을 물음표(“?”)로 표시한다. 사용자의 입력값이 쿼리의 일부로 직접적으로 결합되지 않기 때문에, 악의적인 SQL 코드가 주입되는 것을 방지할 수 있다. Spring Boot에서는 prepareStatement() 메소드를 사용하여 PreparedStatement를 생성하고, 이를 통해 사용자 제공 값을 안전하게 쿼리에 삽입할 수 있다.
아래 예시는 사용자가 제공한 customerId를 안전하게 쿼리에 삽입한다.
String sql = "SELECT * FROM accounts WHERE customerId = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, customerId); // customerId는 사용자 입력 값
ResultSet rs = pstmt.executeQuery();
4-2. JPA와 Hibernate의 역할
- JPA와 Hibernate 같은 ORM(Object-Relational Mapping) 툴은 SQL 쿼리 작성을 간소화하지만, SQL 인젝션을 완전히 방지하지는 못한다. 이를 보완하기 위해 JPA Criteria API를 사용하는 것이 좋다. 이 API는 복잡한 쿼리를 안전하게 구성할 수 있도록 도와주며, 직접적인 SQL 구문을 작성하는 위험을 줄여준다.
아래 예시코드에서는 customerId를 기반으로 계정을 검색한다.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Account> cq = cb.createQuery(Account.class);
Root<Account> account = cq.from(Account.class);
cq.select(account).where(cb.equal(account.get("customerId"), customerId));
TypedQuery<Account> query = entityManager.createQuery(cq);
List<Account> result = query.getResultList();
코드 설명
1. CriteriaBuilder를 사용하여 쿼리를 구성하는 기준을 만든다.
2. CriteriaQuery 객체를 생성하여 어떤 데이터를 조회할지 정의한다. 여기서는 Account 클래스의 데이터를 조회한다.
3. Root 객체를 이용해 특정 엔티티(Account)에 대한 쿼리를 정의한다.
4. where 절을 사용하여 검색 조건을 추가한다. 이 경우 customerId가 일치하는 계정을 찾는다.
5. 마지막으로 TypedQuery를 생성하여 준비된 쿼리를 실행하고 결과를 가져온다.
4-3. 사용자 입력 데이터 검증 및 새니타이징
- 안전한 웹 어플리케이션을 구축하기 위해서는 사용자로부터 받은 데이터를 검증하고 새니타이징하는 것이 중요하다. 데이터 새니타이징은 사용자 데이터에 필터를 적용하여 어플리케이션의 다른 부분에서 안전하게 사용할 수 있게 하는 기술이다. 이 과정에서 화이트리스트(안전한 것으로 간주되는 입력값만 허용)나 블랙리스트(금지된 패턴 식별)를 사용할 수 있다. 또한, 최소 권한 원칙을 적용하고, 데이터베이스 특정 메소드 사용, 단기적 자격 증명 사용, 로깅, 웹 애플리케이션 방화벽(WAF) 또는 유사한 침입 탐지 솔루션을 사용하는 것도 좋은 방법이다.
데이터 새니타이징에는 입력값을 허용된 값 목록(화이트리스트)과 비교하는 방법이 포함된다. 예를 들어, 허용된 카테고리 목록을 사용하여 사용자 입력을 검증할 수 있다.
Set<String> validCategories = new HashSet<>(Arrays.asList("Electronics", "Books", "Clothing"));
if (validCategories.contains(userInputCategory)) {
// 데이터베이스 쿼리 실행
} else {
// 입력값이 유효하지 않음
}
이번에는 쿼리 인젝션에 대해서 알아봤다. 이게 위험성이 얼마나 큰지에 대해서는 인지를 했다. 하지만 아직 내가 이걸 방지하기 위해서 어떻게 해야할지에 대해서는 제대로 알지 못하는 느낌이라 앞으로 조금 더 자세히 알아볼 생각이다.
5. 쿼리 인젝션에 관한 내용 출처
스프링은 어떻게 생성자 주입을 진행할까? 궁금하다면 아래의 글을 읽어보자!
시간이 괜찮다면 팀원 '평양냉면7'님의 블로그도 한번 봐주세요 :)
'기타 > WEB, DB, GIT' 카테고리의 다른 글
HTTP요청과 멱등성 이해하기: GET부터 DELETE까지 (29) | 2024.01.09 |
---|---|
주니어 개발자의 API 이해하기 (4) | 2023.12.20 |
Jenkins 깃허브 훅 설정 - GitHub hook trigger for GITScm Polling 설정하기 (0) | 2023.10.26 |
Github Access Token발급받는 방법 (2) | 2023.10.26 |
IndexedDB: CSR 데이터베이스 (0) | 2023.10.20 |