Programming/Spring

MyBatis SQL Injection 발생하는 상황 및 방어 방법

Jan92 2024. 4. 10. 20:14

MyBatis SQL Injection 발생하는 상황 및 ${ }, #{ } 바인딩 차이

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이 발생하지 않는 안전성뿐만 아니라 성능적 이점도 가진다는 특징이 있습니다.