Learning Web/2024~2025 Web Hacking

로그인 페이지에서 SQL injection 방지 : prepared statement

naiLED 2025. 2. 3. 14:26

로그인 페이지(login.php)를 작성하는데, 사용자 정보를 확인할 때 다음과 같은 코드를 작성한 것을 볼 수 있다.

 

// DB와 연결하기
$servername = "localhost"; 
$username = "root"; 
$password = ""; 
$dbname = "my_db"; 

$conn = new mysqli($servername, $username, $password, $dbname);

$sql = "SELECT id, password FROM users WHERE user_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $user_id);
$stmt->execute();
$result = $stmt->get_result();

 

이는 SQL injection을 방지하기 위한 prepared statement이다.

코드 한 줄 한 줄 해석하기에 앞서, 왜 prepared statement를 사용해야 하는지부터 살펴보겠다.

 

0-1. 만약 Prepared Statement가 없다면?

 

입력받은 로그인 정보와 DB에 저장된 users data를 비교하기 위해서, 가장 간단하게는 이런 방식으로 작성할 수 있다.

$sql = "SELECT id, password FROM users WHERE user_id = '$user_id'";
$result = $conn->query($sql);

 

첫 줄에서는 users 테이블에서 user_id값을 기준으로 데이터를 조회하고, 여기서 id와 password를 SELECT 쿼리로 가져온다. 그 다음, $conn 객체의 query() 함수를 실행하는데, 여기서 첫 줄에 만들어둔 SQL 쿼리문($sql)을 실행하고 그 값을 $result에 저장한다.

 

이 때, 사용자가 user_id에 'OR '1'='1 를 입력한다고 가정하면, SQL 인젝션이 가능해지는 것을 확인할 수 있다.

 

//보안 위험 예시
SELECT id, password FROM users WHERE user_id = '' OR '1'='1';

 

이렇게 되면 WHERE 조건이 항상 true가 되므로, 모든 사용자 정보를 가져올 수 있어 해커가 모든 계정 정보를 조회할 수도 있다.

 

 

0-2. Prepared Statement의 일반적인 Flow

 

prepared statement는 일반적으로 다음과 같은 단계를 거친다.

 

1) 준비(prepare) : 쿼리의 틀을 만들고, 이때 특정값 대신 ?(바인딩 변수)를 사용하여 placeholder를 남긴다.

2) 컴파일 및 최적화(compile & optimize) : 쿼리의 틀을 컴파일(최적화 및 변환)하며 아직 실행하지 않고 결과만 저장한다.

3) 바인딩(binding) : 바인딩 변수를 설정하여, ?에 들어갈 실제 값을 연결한다.

4) 실행(execute) : 실제 SQL 실행이 이루어진다.

 

이제 다시 맨 앞으로 돌아가서, 코드 한 줄 한 줄 살펴보자.

 

1.  준비(prepare) :  바인딩 변수 '?' 사용

$sql = "SELECT id, password FROM users WHERE user_id = ?";

 

첫 줄은 기존 statement와 거의 똑같은데, '$user_id' 를 바로 입력하지 않고 '?'를 입력했다.

이는 '바인딩 변수' 라고 하는데, SQL에서 값을 직접 넣지 않고 안전하게 처리하기 위해 사용하는 변수다.

 

 

2. 준비(prepare), 컴파일 및 최적화(compile & optimize) : Prepare() 사용

$stmt = $conn->prepare($sql);

 

MySQL에게 "이 SQL을 준비해" 라고 요청한다.

이는 바로 실행하지 않고, 나중에 안전하게 실행하기 위해 필요한 과정이다.

 

 

3. 바인딩(binding) : bind_param() 사용

$stmt->bind_param("s", $user_id);
 

 

드디어 ? 자리에 $user_id 값을 넣는다.

여기서 "s" 는 문자열(string) 타입의 값을 의미한다.

 

 

4. 실행(execute) : execute() 사용

$stmt->execute();

 

이제 execute()를 호출하면 SQL이 실행된다.

bind_param() 에서 받은 값을 포함해서 실행된다.

 

 

5. 결과 반환

$result = $stmt->get_result();

 

실행된 쿼리의 결과를 객체 형태로 가져온다.