MyBatis SQL Injection 발생하는 상황 및 ${ }, #{ } 바인딩 차이
1.SQL Injection 이란?
데이터베이스(Database)와 연동된 웹사이트 및 웹 애플리케이션에서 공격자가 입력 폼 또는 URL을 통해 SQL 쿼리문을 전송하여 데이터베이스를 열람 또는 조작하는 해킹 기법을 말합니다.
SQL Injection 공격의 종류에는 크게 '인증 우회(Auth Bypass)', '데이터 노출(Data Disclisure)', '원격명령 실행(Remote Command Excute)' 세 가지가 있습니다.
해당 공격들은 클라이언트로부터 입력되는 데이터의 유효성을 제대로 필터링하지 않아 발생하며, 쉬운 공격 난이도에 비해 상당한 피해를 줄 수 있기 때문에 SQL Injection을 예방할 수 있는 시큐어 코딩(Secure Coding)은 필수적이면서 또 기본적이라고 할 수 있습니다.
2. MyBatis 데이터 바인딩 방법 ${ }, #{ }
MyBatis에서 사용되는 데이터 바인딩 방법은 '${}'를 통한 방법과 '#{}'를 통한 방법이 있습니다.
'${}'를 통한 바인딩의 경우 'Statement' 방식을 기반으로 동작하며 SQL Injection이 발생할 수 있기 때문에 사용 시 유의해야 하며, '#{}'를 통한 바인딩의 경우 'PreparedStatement' 방식을 기반으로 동작하며 SQL Injection이 발생하지 않습니다.
아래 내용을 통해 각각의 방식에 대한 비교와 함께 SQL Injection이 발생하는 상황에 대해서도 살펴보겠습니다.
2-1. '${ }' 바인딩 방식과 SQL Injection 발생 예시
//1. Service 부분 코드
Map<String, Object> resultMap = userDao.getUserById("inputId");
//2. DAO 부분 코드
public Map<String, Object> getUserById(String id) {
return this.sqlSessionTemplate.selectOne("UserMapper.getUserById", id);
}
//3. Mapper 부분 코드
<select id="getUserById" resultType="map">
SELECT * FROM user WHERE id = ${userId}
</select>
먼저 '${}' 바인딩의 경우 동적으로 SQL 쿼리를 생성해야 할 때 사용되며, 보시는 결과처럼 입력되는 값을 그대로 바인딩하기 때문에 예시와 같이 사용하는 경우 오류가 발생합니다.
오류가 발생하지 않기 위해서는 Service 단에서 getUserById 메서드 호출 시 파라미터를 ("'inputId'")와 같이 넘겨주어야 정상적으로 동작하게 됩니다.
(SELECT * FROM user WHERE id = 'inputId')
//Mapper 부분 코드
<select id="getUserById" resultType="map">
SELECT * FROM user WHERE id = '${userId}'
</select>
또는 Mapper의 코드 상에서 '${userId}'와 같이 따옴표를 붙어 ("inputId")와 같이 데이터를 넘겨주는 방식으로도 사용할 수 있습니다.
이처럼 입력되는 파라미터를 그대로 바인딩하는 방식으로 인해 SQL Injection이 발생할 수 있는데요.
//1. Service 부분 코드
Map<String, Object> inData = new HashMap<String, Object>();
inData.put("userId", "inputId");
inData.put("userPassword", "inputPw");
Map<String, Object> resultMap = userDao.getUserByIdPassword(inData);
//2. DAO 부분 코드
public Map<String, Object> getUserByIdPassword(Map<String, Object> map) {
return this.sqlSessionTemplate.selectOne("UserMapper.getUserByIdPassword", map);
}
//3. Mapper 부분 코드
<select id="getUserByIdPassword" resultType="map">
SELECT * FROM user WHERE id = '${userId}' AND password = '${userPassword}'
</select>
만약 다음과 같은 로그인 과정에서 사용자가 아래와 같은 password 값을 입력한다면 'or 1=1' 부분으로 인해 해당 쿼리는 absolutely true가 되고 아이디와 비밀번호를 아무거나 입력하더라도 로그인이 되는 상황이 발생하게 됩니다.
//사용자가 악의적인 목적으로 다음과 같은 password를 입력한다면
inData.put("userPassword", "any' OR 1=1 limit 1 -- ");
//실행되는 쿼리
SELECT * FROM user WHERE id = 'inputId' AND password = 'any' OR 1=1 limit 1 -- '
(예시의 이해를 위해 암호화는 생략하였다는 점 참고 부탁드리며, -- 를 통해 마지막 ' 따옴표를 주석처리하여 쿼리를 정상 동작시켰습니다.)
결론적으로 mybatis에서 '${}'를 사용한 바인딩의 경우 위 예시 외에도 여러 가지 SQL Injection을 발생시킬 수 있다는 문제점이 있습니다.
2-2. '#{ }' 바인딩 방식과 SQL Injection이 발생하지 않는 이유
//1. Service 부분 코드
Map<String, Object> inData = new HashMap<String, Object>();
inData.put("userId", "inputId");
inData.put("userPassword", "inputPw");
Map<String, Object> resultMap = userDao.getUserByIdPassword(inData);
//2. DAO 부분 코드
public Map<String, Object> getUserByIdPassword(Map<String, Object> map) {
return this.sqlSessionTemplate.selectOne("UserMapper.getUserByIdPassword", map);
}
//3. Mapper 부분 코드
<select id="getUserByIdPassword" resultType="map">
SELECT * FROM user WHERE id = #{userId} AND password = #{userPassword}
</select>
다음으로 '#{}' 바인딩의 경우 위 결과에서 볼 수 있는 것처럼 PreparedStatement 방식이 적용되기 때문에 입력 값을 안전하게 바인딩할 수 있으며, SQL Injection 공격에 노출되지 않습니다.
public void preparedStatementExample(int idx, String id, String name) {
Connection con = null;
PreparedStatement preparedStatement = null;
try {
String url = "jdbc:mysql://localhost:3306/spring_mvc?serverTimezone=UTC";
con = DriverManager.getConnection(url, "username", "password");
preparedStatement = con.prepareStatement("insert into user values(?, ?, ?)");
preparedStatement.setInt(1, idx);
preparedStatement.setString(2, id);
preparedStatement.setString(3, name);
preparedStatement.execute();
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
con.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
추가적으로 다음 코드를 통해 PreparedStatement 방식이 동작하는 원리 및 장점을 간단하게 살펴보자면,
Statement, PreparedStatement 방식 모두 SQL 문장을 컴파일하고 최적화한 뒤 실행 전에 캐싱하는 작업을 거치지만, Statement 방식의 경우 수행했던 쿼리와 완전히 일치하는 쿼리에 대해서만 캐싱된 데이터를 재활용할 수 있으며, PreparedStatement 방식의 경우 '?'를 통한 'placeHolder' 방식을 통해 SQL 문을 정의하기 때문에 한번 분석되면 이후 재사용이 용이하다는 장점이 있습니다.
(Statement, PreparedStatement의 차이점에 대해서는 구글링 하면 잘 정리된 내용이 많기 때문에 참고해 보셔도 좋을 것 같습니다.)
결론적으로 상황에 따라 '${}' 바인딩 방법이 필요한 경우도 있겠지만 이 경우에는 SQL Injection이 발생할 수 있기 때문에 그에 대한 조치가 필요하며, '#{}' 바인딩 방법은 PreparedStatement 방식으로 인해 SQL Injection이 발생하지 않는 안전성뿐만 아니라 성능적 이점도 가진다는 특징이 있습니다.
'Programming > Spring' 카테고리의 다른 글
Logback PatternLayout을 통한 로그 마스킹 처리 방법 (1) | 2024.11.02 |
---|---|
(spring project) log4j2 로그 파일 분리하기 (1) | 2024.08.25 |
spring + oracle(ojdbc) 연결 방법 정리 (0) | 2024.03.30 |
mybatis selectKey 사용 방법 및 주의할 점 (selectKey 다중 컬럼) (0) | 2024.03.20 |
ObjectMapper는 Bean으로 등록해서 사용하자 (0) | 2024.03.12 |