SQL injection
악의적인 SQL 코드를 데이터베이스 쿼리에 삽입하는 공격 기법이다.
이 공격을 통해 공격자는 데이터베이스를 조작하여 민감한 정보를 획득하거나 삭제, 변경하는 등의 행위를 할 수 있다.
SQL 인젝션은 웹 애플리케이션의 보안 취약점 중 하나로, 입력 값 검증이나 적절한 쿼리 구조의 사용 없이 사용자 입력을 데이터베이스 쿼리에 직접 포함시킬 때 발생한다.
공격 방법
•
사용자 입력을 통한 공격
◦
사용자로부터 받은 입력값을 필터링 없이 SQL 쿼리에 포함시킬 때, 공격자는 악의적인 코드를 삽입하여 원하지 않는 쿼리를 실행시킬 수 있다.
•
로그인 폼 우회
◦
공격자는 SQL 인젝션을 이용해 인증 과정을 우회하여 무단으로 시스템에 접근할 수 있다.
•
데이터 유출
◦
민감한 정보가 저장된 데이터베이스에서 데이터를 추출할 수 있다.
•
데이터베이스 조작
◦
데이터베이스에 저장된 데이터를 수정하거나 삭제할 수 있다.
해결 방법
•
사용자 입력 검증
◦
서버 측에서 철저하게 검증.
•
PreparedStatement 사용
◦
PreparedStatement를 사용해서 SQL 인젝션을 방지할 수 있다.
◦
쿼리에 변수를 직접 삽입하는 대신, 미리 컴파일된 쿼리에 파라미터를 전달하는 방식.
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet results = pstmt.executeQuery();
Java
복사
코드 예시에서는 ?를 사용하여 사용자 입력 위치를 표시하고, setString 메소드를 통해 안전하게 값을 설정한다.
•
ORM 사용
◦
ORM 라이브러리를 사용하면 SQL 쿼리를 직접 작성하는 대신 객체 기반의 인터페이스를 통해 데이터베이스를 조작할 수 있어, SQL 인젝션 위험을 줄일 수 있다.
•
웹 애플리케이션 방화벽(WAF) 사용
◦
특정 패턴이나 의심스러운 트래픽을 감지하여 SQL 인젝션 공격을 차단할 수 있다.
SQL 인젝션은 심각한 보안 위협이지만, 적절한 방어 기법을 적용함으로써 효과적으로 방지할 수 있음.
사용자 입력 검증, 안전한 쿼리 작성 방법의 적용, 보안 도구의 사용은 SQL 인젝션 방지를 위한 필수 조치다.
PreparedStatement와 Statement
SQL 인젝션 공격에 대응하기 위한 방법으로 PreparedStatement와 Statement의 사용이 중요하다.
이 두 인터페이스는 자바의 JDBC(Java Database Connectivity) API에서 SQL 쿼리를 실행하는 데 사용된다.
Statement
•
Statement는 SQL 쿼리를 실행하기 위한 기본 인터페이스다.
•
사용자 입력을 쿼리 문자열에 직접 결합하여 SQL 쿼리를 생성한다.
•
동적인 쿼리 실행에 사용되며, SQL 인젝션 공격에 취약하다는 단점이 있다.
예시
Statement stmt = connection.createStatement();
String query = "SELECT * FROM users WHERE username = '" + username + "'";
ResultSet rs = stmt.executeQuery(query);
Java
복사
username 변수에 악의적인 SQL 코드가 포함될 경우 데이터베이스가 공격받을 수 있다.
PreparedStatement
•
PreparedStatement는 Statement를 상속받는 인터페이스로, 미리 컴파일된 SQL 쿼리를 사용한다.
•
쿼리의 구조가 미리 정해져 있고, 실행 시점에 파라미터를 전달하여 쿼리를 실행한다.
•
SQL 인젝션 공격에 대한 보호 기능을 제공한다.
•
사용자 입력이 쿼리의 일부로 결합되지 않음. 오로지 파라미터로 전달되기 때문에, 악의적인 SQL 코드가 쿼리를 변조할 수 없다.
•
성능 측면에서도 이점이 있다. 미리 컴파일된 쿼리를 여러 번 재사용할 수 있으므로, 쿼리 실행 속도가 향상될 수 있다.
예시
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
Java
복사
이 방식에서는 ?를 통해 파라미터의 위치를 지정하고, setString 메소드를 사용하여 안전하게 파라미터 값을 설정한다.
차이점
•
보안
◦
PreparedStatement는 SQL 인젝션 공격에 대해 안전하다.
◦
사용자 입력이 쿼리에 파라미터로 전달되므로, 입력값이 쿼리를 변조할 수 없다.
◦
반면, Statement는 사용자 입력을 쿼리에 직접 결합하기 때문에 SQL 인젝션 공격에 취약하다.
•
성능
◦
PreparedStatement는 미리 컴파일된 SQL 쿼리를 사용하므로, 동일한 쿼리를 반복 실행할 경우 성능상의 이점이 있다.
◦
Statement는 매번 쿼리를 컴파일해야 하므로, 같은 쿼리를 반복 실행할 경우 PreparedStatement보다 성능이 떨어질 수 있다.
•
용도
◦
동적으로 변하는 쿼리를 실행할 경우 Statement를 사용할 수 있지만, 보안과 성능을 고려할 때 PreparedStatement의 사용이 권장된다.
결론적으로, SQL 인젝션 공격을 방지하고 데이터베이스 애플리케이션의 성능을 최적화하기 위해서는 PreparedStatement의 사용이 권장된다.
근데 민감한 정보를 어떻게 가져갈 수 있을까?
창의적인 방법이 여러개 있다.
원래 조건문으로만 사용해야 되는데 종결문을 포함한 쿼리를 새로 붙여 넣어서 이제 앞에 조건은 항상 통과해서 쿼리를 패스시킨다던가 아니면 뒤에 쿼리가 실행되게 해서 데이터를 끌어온다거나…
-- 원래 쿼리
SELECT * FROM users WHERE username = 'user' AND password = 'pass';
-- SQL 인젝션 공격
SELECT * FROM users WHERE username = 'user' OR 1=1 -- AND password = 'pass';
SQL
복사
예전에는 프로그래밍을 하면 ORM 개념이 많이 없어서 사실 이런 방법을 누구나 다 알고 있었다.
요즘에는 ORM이 너무 유명해져서 대부분의 프레임워크를 쓰면 사실 이걸 걱정할 일이 거의 없다.
성능 최적화를 하다 보면은 이제 직접 쿼리를 다루게 될 일이 있어서 모르고 있으면 큰 이슈가 발생할 수 있기 때문에 잘 알고 있어야 한다.