SQL Injection - 방어 실습(2), Prepared Statement
Prepared statement(준비된 쿼리, statement) 방식의 DB 쿼리는 원래 동일한 쿼리를 여러 번 반복 실행할 때 성능을 향상시키기 위해 개발되었다. 쿼리에서 데이터가 들어가는 부분을 다 빼놓고 문법만 분석하여 DB서버가 쿼리 계획을 세우고 최적화할 기회를 주고 나서, 최적화된 쿼리에 데이터를 집어넣어 빠르게 반복 실행하려는 목적이었다.
그런데 여기에 성능보다 더 중요한 보안상의 장점이 있다는 사실이 밝혀지면서 이제는 성능보다 보안 때문에 prepared statement를 사용하는 개발자들이 많다. 보안에 도움이 되는 이유는 데이터를 빼고 쿼리 문법만 먼저 분석하는 단계를 거치기 때문이다. 사용자가 입력한 데이터에 위험한 내용이 들어 있더라도, 쿼리 문법을 분석하는 단계에는 그 데이터가 들어가지 않으므로 절대적으로 안전하다는 것이다.
즉, Prepared Statement를 이용하면 SQL Injection을 막을 수 있다는 것이다.
mysqli로 Prepared Statement를 사용하는 방법은 복잡하고 어려운데, 이런 불편함을 해결하기 위해 PDO가 등장했다. (*PDO(PHP Data Objects)란? 여러가지 데이터베이스를 제어하는 방법을 표준화시킨 것)
따라서 나는 PDO를 사용해 Prepared Statement를 이용한 SQL Injection 방어 코드를 작성했다.
<?php
if(isset($_POST['uid'])&&isset($_POST['pw'])) {
$username=$_POST['uid'];
$userpw=$_POST['pw'];
// prepared statement 사용 X
// $conn= mysqli_connect('localhost', 'root', '000129', 'swsecsql');
// $sql="SELECT * FROM user_info where uid='$username'&&pw='$userpw'";
// prepared statement 사용 (PDO)
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
);
$pdo = new PDO('mysql:host=localhost;dbname=swsecsql;charset=utf8', 'root', '000129', $options);
$sql = $pdo->prepare("SELECT * FROM user_info where uid=? &&pw=?");
$sql->execute(array($username, $userpw));
$result = $sql->fetchAll();
// prepared statement 사용 X
// if($result=mysqli_fetch_array(mysqli_query($conn, $sql))) {
// prepared statement 사용
if($result) {
echo "<div class='welcome'>";
echo "<p class='success_text'>로그인 성공</p>";
echo "<div class='welcome_text'> $username"."님 환영합니다.</div>";
echo "<div class='board_link'><a href='board.html'>게시판 가기</a></div>";
echo "</div>";
} else {
echo "<div class='welcome'>";
echo "<p class='success_text'>로그인 실패</p>";
echo "<div class='welcome_text'>(인증 정보 불일치)</div>";
echo "</div>";
}
}
?>
위 코드는 로그인 페이지에 Prepared Statement를 적용한 것이다. 보다시피 문법을 먼저 전달한 후에 사용자가 입력한 값을 변수 배열에 담아 넘겨주고 있다.
따라서 사용자의 입력값이 문법인 척 끼어들어서 악의적인 작용을 할 수 없게 된다.
Prepared Statement를 적용한 상태에서는 로그인 페이지에 ' or 1=1#을 입력해도 로그인에 실패한다.