SQL Injection (SQL 주입)
데이터베이스에 대해 무단 접근을 하거나, 데이터베이스에서 직접 정보를 검색하여 SQL 명령을 악의적으로 전달하는 행위
SQL Injection을 통해서 데이터베이스에 직접 접근하여 노출되면 안 되는 정보를 검색할 수 있다.
예시를 들자면 다음과 같다.
show databases;
select * from sakila.actor;
SELECT * FROM sakila.actor WHERE first_name = '' OR '1'='1'
SELECT * FROM sakila.actor WHERE first_name = '' OR '1'='1'구문은 SQL Injection의 대표적인 예시이다.
문자열 결합 방식 을 쓰면 안되는 문제인 것이다. PreparedStatement 로 이 SQL Injection을 방지할 수 있다.
PreparedStatement는 SQL 쿼리의 구조와 값을 분리한다.
// 1. DB 연결
connection = DriverManager.getConnection(DB_URL + DATABASE_SCHEMA, USER_NAME, PASSWORD);
// 2. Statement 객체 생성
statement = connection.prepareStatement("SELECT * FROM customer WHERE first_name = ?");
statement.setString(1, inputName);
// 4. 실행
ResultSet rs = statement.executeQuery();
이 코드는 PreparedStatement를 사용하여 쿼리를 실행하는 방식으로, SQLInjection을 방지할 수 있다. Statement는 문자열 결합방식을 사용하지만, PreparedStatement는
쿼리 구조가 먼저 컴파일 되어있고, 이후 값만 바인딩하기 때문이다.
이제 직접 코드 속으로 들어가 보자. 문자열로 치환하는 코드는 어디에 있는 걸까?
public static StringBuilder escapeString(StringBuilder buf, String x, boolean useAnsiQuotedIdentifiers, CharsetEncoder charsetEncoder) {
int stringLength = x.length();
buf.append('\'');
//
// Note: buf.append(char) is _faster_ than appending in blocks, because the block append requires a System.arraycopy().... go figure...
//
for (int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch (c) {
case 0: /* Must be escaped for 'mysql' */
buf.append('\\');
buf.append('0');
break;
case '\n': /* Must be escaped for logs */
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '\'':
buf.append('\'');
buf.append('\'');
break;
case '"': /* Better safe than sorry */
if (useAnsiQuotedIdentifiers) {
buf.append('\\');
}
buf.append('"');
break;
case '\032': /* This gives problems on Win32 */
buf.append('\\');
buf.append('Z');
break;
case '\u00a5':
case '\u20a9':
// escape characters interpreted as backslash by mysql
if (charsetEncoder != null) {
CharBuffer cbuf = CharBuffer.allocate(1);
ByteBuffer bbuf = ByteBuffer.allocate(1);
cbuf.put(c);
cbuf.position(0);
charsetEncoder.encode(cbuf, bbuf, true);
if (bbuf.get(0) == '\\') {
buf.append('\\');
}
}
buf.append(c);
break;
default:
buf.append(c);
}
}
buf.append('\'');
return buf;
}
ResultSet rs = statement.executeQuery(); 이 코드를 breakPoint로 디버깅 했더니, 이 코드가 나왔다.
이 코드를 보면, 특수 문자별 이스케이프 처리를 하는 코드이다.
즉, SELECT * FROM customer WHERE first_name = '\' OR \'1\'=\'1'; 이렇게 escape되기 때문에, '\' OR \'1\'=\'1'가 그냥 문자열로 처리된다.
즉, PreparedStatement를 사용하면, 쿼리 구조와 값을 분리하면서 내부적으로 escape처리를 하므로 SQL Injection 공격에 안전하다.
그렇다면, Statement를 사용하는 대신 무조건 PreparedStatement를 사용하는 것이 나을까?
PreparedStatment의 단점도 존재한다.
1. 동적으로 SQL이 생성된 쿼리에 비해, 쿼리 구조와 값을 바인딩하다보니, 복잡한 조건을 갖고 있는 쿼리문에서는Statement보다 더 느리게 동작할 수 있다.
2. PreparedStatement는 쿼리 구조와, 파라미터 정보를 모두 저장하기 때문에메모리가 소비된다. 자원이 제한된 환경에서는 이러한 메모리 사용이 부담될 수 있다.
3. sql구조 자체를 많이 바꿔야 하는 경우, PreparedStatement로 관리하기 번거로울 수 있다.
이렇게 PreparedStatment의 코드를 공부해보니, 무조건 PreparedStatement로 사용하는 것이 아니라, 때에 따라 더 적합한 것을 찾아 사용해야겠다는 것을 배웠다.
우와! 신기해요!