Prepared Statements를 이용한 SQL Injection 방어
Min-jun Kim
Dev Intern · Leapcell

소개
오늘날 데이터 중심의 세상에서 거의 모든 애플리케이션은 중요한 정보를 저장하고 검색하기 위해 데이터베이스에 의존합니다. 사용자 자격 증명부터 금융 거래에 이르기까지, 이 데이터의 무결성과 보안은 매우 중요합니다. 그러나 이러한 의존성은 악의적인 행위자에게 상당한 공격 표면을 제공하기도 합니다. 웹 애플리케이션에서 가장 널리 퍼지고 위험한 취약점 중 하나는 SQL Injection입니다. 이 공격 벡터는 비인가된 데이터 액세스, 수정 또는 심지어 완전한 데이터베이스 파괴로 이어질 수 있으며, 기업과 사용자에게 막대한 피해를 초래합니다. SQL Injection이 어떻게 작동하는지, 그리고 더 중요하게는 어떻게 예방하는지 이해하는 것은 보안 애플리케이션을 구축하는 모든 개발자에게 매우 중요합니다. 이 글에서는 SQL Injection의 핵심 원리를 살펴보고, 매개변수화된 쿼리, 즉 Prepared Statements가 제공하는 강력한 방어를 시연할 것입니다.
위협 이해하기: SQL Injection
예방에 대해 자세히 알아보기에 앞서, 관련된 핵심 개념을 명확히 이해하는 것이 중요합니다.
핵심 용어
- SQL (Structured Query Language): 관계형 데이터베이스와 통신하고 조작하는 데 사용되는 표준 언어입니다. 데이터 정의, 쿼리 및 업데이트에 사용됩니다.
- 데이터베이스 쿼리: 정보를 검색하거나 특정 작업을 수행하기 위해 데이터베이스에 보내는 요청입니다(예: 데이터 검색, 새 데이터 삽입, 기존 데이터 업데이트).
- 사용자 입력: 일반적으로 양식, URL 매개변수 또는 API 요청을 통해 애플리케이션 사용자가 제공하는 모든 데이터입니다.
- SQL Injection: 애플리케이션의 데이터베이스 상호 작용에서 취약점을 악용하는 코드 삽입 기법입니다. 공격자는 악의적인 SQL 코드를 입력 필드에 삽입하고, 이 코드는 데이터베이스에 의해 실행됩니다.
SQL Injection의 원리
SQL Injection은 애플리케이션이 사용자 제공 입력을 적절한 유효성 검사 또는 정리 없이 직접 연결하여 SQL 쿼리를 구성할 때 발생합니다. 이를 통해 공격자는 원래 쿼리의 논리를 조작할 수 있습니다.
애플리케이션이 사용자 이름과 암호를 기반으로 사용자를 인증한다고 가정해 봅시다. 일반적이지만 취약한 쿼리는 다음과 같습니다.
SELECT * FROM users WHERE username = 'userInputUsername' AND password = 'userInputPassword';
공격자가 사용자 이름 필드에 ' OR '1'='1
을 입력하고 암호 필드에는 아무거나 입력한다고 가정해 봅시다.
결과 SQL 쿼리는 다음과 같이 됩니다.
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anyPassword';
'1'='1'
은 항상 참이므로 WHERE 절이 참이 되어 암호 확인을 효과적으로 우회하고, 종종 관리자인 users
테이블의 첫 번째 사용자로 공격자가 로그인할 수 있게 합니다.
간단한 인증 우회 외에도 SQL Injection은 다음과 같은 결과를 초래할 수 있습니다.
- 데이터 유출: 데이터베이스에서 민감한 데이터 검색.
- 데이터 조작: 기존 데이터 수정 또는 삭제.
- 권한 상승: 더 높은 수준의 데이터베이스 권한 획득.
- 원격 코드 실행: 경우에 따라 데이터베이스 서버에서 임의 명령 실행.
해결책: 매개변수화된 쿼리 (Prepared Statements)
SQL Injection에 대한 가장 효과적이고 널리 권장되는 방어책은 매개변수화된 쿼리, 즉 Prepared Statements를 사용하는 것입니다.
Prepared Statements 작동 방식
Prepared Statements는 SQL 쿼리 구조와 실제 사용자 제공 데이터를 분리하여 작동합니다. 사용자 입력을 SQL 문자열에 직접 삽입하는 대신 플레이스홀더가 쿼리에 사용됩니다. 그런 다음 데이터베이스는 사용자 입력이 플레이스홀더에 바인딩되기 전에 이 쿼리 구조를 컴파일합니다. 매개변수(사용자 데이터)가 최종적으로 제공되면 데이터베이스는 이를 실행 가능한 SQL 코드처럼 취급하지 않고 엄격하게 데이터 값으로 취급합니다. 이를 통해 사용자 입력이 쿼리의 논리적 구조로부터 완전히 분리되어 삽입 가능성을 제거합니다.
과정은 다음과 같습니다.
- 준비: 애플리케이션은 동적 값에 대한 플레이스홀더(예:
?
또는:param_name
)를 사용하여 템플릿 SQL 쿼리를 데이터베이스에 보냅니다. - 컴파일: 데이터베이스는 이 쿼리 템플릿을 파싱, 컴파일 및 최적화합니다. 플레이스홀더가 SQL 논리의 일부가 아닌 데이터임을 이해합니다.
- 매개변수 바인딩: 그런 다음 애플리케이션은 실제 사용자 제공 데이터를 별도로 제공하여 플레이스홀더에 바인딩합니다.
- 실행: 데이터베이스는 사전 컴파일된 쿼리를 실행하여 제공된 데이터를 안전하게 통합합니다.
코드 예제
일반적인 프로그래밍 언어로 된 실제 예제를 통해 이를 설명해 보겠습니다.
Python (PostgreSQL용 psycopg2
사용)
import psycopg2 try: conn = psycopg2.connect( host="localhost", database="mydatabase", user="myuser", password="mypassword" ) cur = conn.cursor() # --- 취약한 접근 방식 (사용하지 마십시오) --- # username_input = "admin' OR '1'='1" # password_input = "any" # vulnerable_query = f"SELECT * FROM users WHERE username = '{username_input}' AND password = '{password_input}';" # print(f"Vulnerable Query: {vulnerable_query}") # cur.execute(vulnerable_query) # print("Vulnerable result:", cur.fetchall()) # --- 안전한 접근 방식: Prepared Statements --- username_input = "admin' OR '1'='1" # 공격자의 시도 password_input = "password123" # 플레이스홀더 사용 (%s) secure_query = "SELECT * FROM users WHERE username = %s AND password = %s;" print(f"\nSecure Query Template: {secure_query}") print(f"Parameters: ('{username_input}', '{password_input}')") cur.execute(secure_query, (username_input, password_input)) result = cur.fetchall() if result: print("Secure result: User authenticated successfully.") else: print("Secure result: Invalid credentials or user not found.") conn.commit() except Exception as e: print(f"An error occurred: {e}") finally: if conn: cur.close() conn.close()
안전한 Python 예제에서 %s
는 플레이스홀더 역할을 합니다. cur.execute()
를 secure_query
와 튜플 (username_input, password_input)
로 호출하면, psycopg2
는 username_input
과 password_input
이 내용에 관계없이 리터럴 문자열 값으로 취급되도록 보장하여 ' OR '1'='1'
부분이 SQL 코드로 해석되는 것을 효과적으로 방지합니다.
Java (JDBC 사용)
import java.sql.*; public class JdbcPreparedStatements { public static void main(String[] args) { String url = "jdbc:postgresql://localhost:5432/mydatabase"; String user = "myuser"; String password = "mypassword"; try (Connection con = DriverManager.getConnection(url, user, password)) { // --- 취약한 접근 방식 (사용하지 마십시오) --- // String usernameInput = "admin' OR '1'='1"; // String passwordInput = "any"; // String vulnerableSql = "SELECT * FROM users WHERE username = '" + usernameInput + "' AND password = '" + passwordInput + "'"; // System.out.println("Vulnerable Query: " + vulnerableSql); // Statement stmt = con.createStatement(); // ResultSet rsVulnerable = stmt.executeQuery(vulnerableSql); // while (rsVulnerable.next()) { // System.out.println("Vulnerable result: " + rsVulnerable.getString("username")); // } // --- 안전한 접근 방식: Prepared Statements --- String usernameInput = "admin' OR '1'='1"; // 공격자의 시도 String passwordInput = "password123"; String secureSql = "SELECT * FROM users WHERE username = ? AND password = ?;"; System.out.println("\nSecure Query Template: " + secureSql); System.out.println("Parameters: ('" + usernameInput + "', '" + passwordInput + "')"); try (PreparedStatement pstmt = con.prepareStatement(secureSql)) { pstmt.setString(1, usernameInput); // 첫 번째 매개변수 설정 pstmt.setString(2, passwordInput); // 두 번째 매개변수 설정 ResultSet rs = pstmt.executeQuery(); if (rs.next()) { System.out.println("Secure result: User authenticated successfully."); } else { System.out.println("Secure result: Invalid credentials or user not found."); } } } catch (SQLException e) { System.err.println("Database error: " + e.getMessage()); } } }
Java 예제에서는 ?
가 플레이스홀더 역할을 합니다. setString(index, value)
와 같은 PreparedStatement
메서드는 실제 데이터를 바인딩하는 데 사용됩니다. 데이터베이스는 쿼리 템플릿과 매개변수를 별도로 수신하여 악의적인 문자열이 SQL 명령으로 해석되는 것을 방지합니다.
적용 시나리오
사용자 제공 또는 외부 데이터를 포함하는 모든 데이터베이스 작업에 매개변수화된 쿼리를 사용해야 합니다. 여기에는 다음이 포함됩니다.
- SELECT 문: 검색어, 사용자 ID 또는 필터링 기준에 기반한 쿼리.
- INSERT 문: 양식 데이터로 새 레코드 추가.
- UPDATE 문: 사용자 입력에 기반한 기존 레코드 수정.
- DELETE 문: 사용자 지정 조건에 기반한 레코드 제거.
Prepared Statements를 데이터베이스 상호 작용의 기본 방법 as adopt하는 것이 좋습니다. 이는 SQL Injection 취약점의 공격 표면을 크게 줄여줍니다.
결론
SQL Injection은 사용자 입력과 SQL 쿼리의 안전하지 않은 연결에서 비롯되는 데이터베이스 보안에 대한 심각한 위협으로 남아 있습니다. 이 원리는 간단합니다. 악의적인 SQL이 실행되도록 애플리케이션을 속여 공격자가 데이터에 대한 비인가된 제어를 얻을 수 있도록 합니다. 강력하고 보편적으로 권장되는 대응책은 쿼리 논리와 데이터 값을 엄격하게 분리하여 삽입 시도를 효과적으로 무력화하는 매개변수화된 쿼리, 즉 Prepared Statements를 구현하는 것입니다. 외부 입력이 포함된 모든 데이터베이스 작업에는 항상 Prepared Statements를 사용하여 데이터를 보호하십시오.