두 가지 방법으로 동일한 작업을 수행하는것을 예로들자.
1) 동적 SQL
PreparedStatement stmt = conn.createStatement("INSERT INTO students VALUES('" + user + "')");
stmt.execute();
2) SQL바인딩
PreparedStatement stmt = conn.prepareStatement("INSERT INTO students VALUES(?)");
stmt.setString(1, user);
stmt.execute();
만약 "user" 변수가 사용자 입력에서 왔고, 그 사용자 입력이 다음과 같았다면:
Robert'); DROP TABLE students; --
첫 번째 경우에는 문제가 발생할 것이지만
두 번째 경우에는 안전하게 "Little Bobby Tables"가 학교에 등록될 것.
SQL Injection을 방지하는 PreparedStatement의 작동 원리를 이해하려면 SQL 쿼리 실행의 단계를 이해해야 한다.
SQL 서버 엔진이 쿼리를 받으면 아래의 단계를 거쳐야 한다.
이 단계에서는 쿼리의 문법과 의미가 검사. 쿼리에 사용된 테이블과 열이 존재하는지 확인
이 단계에서는 쿼리에 사용된 select, from, where 등의 키워드가 기계가 이해할 수 있는 형식으로 변환
쿼리를 실행할 수 있는 여러 방법과 각 방법에 따른 비용이 계산되며, 쿼리를 실행하는 데 가장 적합한 계획을 선택한다.
쿼리 최적화 계획에서 선택된 최적의 계획은 캐시에 저장된다
따라서 동일한 쿼리가 다음에 들어올 때는 1단계, 2단계, 3단계를 다시 거칠 필요가 없다
쿼리가 다시 들어오면 캐시를 확인하여 캐시에서 바로 실행할 계획을 가져온다
이 단계에서는 제공된 쿼리가 실행, 데이터가 ResultSet 객체로 사용자에게 반환
변수를 쿼리에 직접 삽입하는 방식은 SQL 인젝션 공격에 취약하다.
컴파일 과정에서는 매번 쿼리가 새로 생성되고 해석되기 때문에, 컴파일 단계에서 반복적인 작업이 필요하며, 이로 인해 성능이 저하되기도 한다.
이러한 문제를 방지하기 위해 PreparedStatement와 같은 정적 SQL(Static SQL) 방식을 사용하는 것이 권장된다.
PreparedStatement는 완전한 SQL 쿼리가 아니다.
PreparedStatement는 쿼리가 미리 컴파일되고, 사용자 입력이 자리표시자에 바인딩되기 때문에 SQL 인젝션 공격을 방지할 수 있다!
PreparedStatement가 포함된 쿼리가 SQL 서버 엔진에 전달될 때는 다음 단계를 거친다
예를 들어, UPDATE user SET username=? AND password=? WHERE id=?라는 쿼리는 자리표시자가 포함된 상태로 파싱되고 컴파일되며, 최적화되고 캐시에 저장.
이미 컴파일된 상태이므로 런타임에 사용자의 입력이 들어오면 미리 컴파일된 쿼리가 캐시에서 꺼재져서 사용자 자리표시자가 데이터로 대체된다.
중요한 점은, 자리표시자가 사용자 데이터로 대체된 후에는 최종 쿼리가 다시 컴파일되거나 해석되지 않으며, SQL 서버 엔진은 사용자 데이터를 순수 데이터로 처리하여, SQL로 해석하거나 다시 컴파일하지 않는다는 점이다. 이것이 바로 SQLinjection을 막는 PreparedStatement의 뛰어난 점!!
쿼리가 다시 컴파일 단계를 거치지 않기 때문에, 자리표시자에 대체된 데이터는 순수 데이터로 처리되며 SQL 서버 엔진에게는 아무 의미가 없으며, 쿼리를 직접 실행.
PreparedStatement는 한 번만 컴파일되는 특징 덕분에 SQL Injection 공격으로부터 안전하다!