mysqli의 bind_param 사용하기

이 포스트에서는 PHP에서 mysqli의 bind_param을 사용하는 방법에 대해 포스팅합니다.

먼저, 간단하게 할 말을 남기는 프로그램을 작성해 봅시다. 일련번호로 ‘no'(int), 댓글로 ‘comment'(varchar 255)라는 이름의 테이블 ‘comments’가 있다고 가정합니다.
댓글을 남기기 위한 폼은 다음과 같이 작성합니다. (전체적인 코드는 편의상 생략)

<form method="POST" action="comment_ok.php">
	<p>하고 싶은 말:
		<input type="text" name="comment" style="width: 400px;" />
		<input type="submit" value="남기기" />
	</p>
</form>

그리고 위의 질의를 보낼 comment_ok.php라는 프로그램을 다음과 같이 bind_param을 사용하지 않은 채로 작성해 봅시다.

<?php
	/* Load DB */
	$conn = mysqli_connect('localhost', 'id', 'passwd', 'hello');
	if ( !$conn ) die('DB Error');

	/* Set to UTF-8 Encoding */
	mysqli_query($conn, 'set session character_set_connection=utf8;');
	mysqli_query($conn, 'set session character_set_results=utf8;');
	mysqli_query($conn, 'set session character_set_client=utf8;');
	
	if ( isset($_POST['comment']) ) {
		$comment = trim($_POST['comment']);
		if ($comment) {
			$query = "INSERT INTO comments (comment) VALUES ('" . $comment . "')";
			mysqli_query($conn, $query);
		}
	}
	mysqli_close($conn);
?>
<meta http-equiv="refresh" content="0;url=comment.php" />

위에서 강조 표시한 14번과 15번 줄이 문제가 될 부분입니다.

댓글을 남기기 위한 comment.php 페이지로 접속 후 댓글 입력 폼이 뜨면 ‘Hello, world!’라고 입력한 후 서버측에서 SQL 프로그램을 실행시켜

SELECT * FROM comments;

위와 같은 쿼리를 입력해 보면,

+----+---------------+
| no | comment       |
+----+---------------+
|  1 | Hello, world! |
+----+---------------+

위와 같이 Hello, world!라고 입력한 사항이 반영되어 있습니다.

여기서 한 번 장난을 쳐 봅시다. 이번에는 댓글 입력란에 [메롱’), (‘메롱] 이렇게 입력해 봅시다. (사각괄호 []는 제외) 그리고 서버측 SQL 프로그램에서

SELECT * FROM comments;

다시 위와 같은 쿼리를 입력해서 조회해 보면,

+----+---------------+
| no | comment       |
+----+---------------+
|  1 | Hello, world! |
|  2 | 메롱          |
|  3 | 메롱          |
+----+---------------+

이렇게 메롱이 두 번 들어갔습니다. 왜냐하면, [Hello, world!]와 같이 입력할 경우는

INSERT INTO comments (comment) VALUES ('Hello, world!');

이와 같이 되어 그냥 정상적으로 ‘Hello, world!’가 한 번 입력됩니다.

하지만 [메롱’), (‘메롱] 이렇게 입력할 경우는

INSERT INTO comments (comment) VALUES ('메롱'), ('메롱');

이렇게 (‘메롱’)이 두 개가 되어 두 줄이 동시에 들어가는 것입니다.

이것이 문제가 되는 이유는 해커가 악의적인 목적을 가지고 이를 악용할 수 있다는 점에 있습니다. 예를 들어, 로그인 단계에서의 검증을 뚫어서 관리자 계정을 탈취한 후 관리자 권한을 남용하거나, 데이터베이스나 테이블을 삭제해 버리는 등 무궁무진한 방법으로 이러한 취약점을 악용하여 사이버 공격을 감행할 수 있습니다. 이를 SQL 인젝션(SQL Injection)이라고 합니다. 데이터베이스에 비정상적인 쿼리를 삽입하여 개발자가 의도하지 않은 방향으로 프로그램을 동작하게 하기 때문에 이런 이름이 붙었습니다.

이러한 SQL 인젝션을 방어하기 위한 대책으로 바인딩(binding)을 생각해 볼 수 있습니다. 이는 쿼리에 값을 직접 입력하지 않고 일단 ‘?’로 처리한 다음 그 자리에 값을 삽입하는 방법입니다. PHP에서는 mysqli_stmt::bind_param을 사용해 바인딩할 수 있습니다.

앞의 comment_ok.php 프로그램을 다음과 같이 다시 작성해 봅시다.

<?php
	/* Load DB */
	$conn = mysqli_connect('localhost', 'id', 'passwd', 'hello');
	if ( !$conn ) die('DB Error');

	/* Set to UTF-8 Encoding */
	mysqli_query($conn, 'set session character_set_connection=utf8;');
	mysqli_query($conn, 'set session character_set_results=utf8;');
	mysqli_query($conn, 'set session character_set_client=utf8;');
	
	if ( isset($_POST['comment']) ) {
		$comment = trim($_POST['comment']);
		if ($comment) {
			$stmt = $conn->prepare("INSERT INTO comments (comment) VALUES (?)");
			$stmt->bind_param('s', $comment);
			$stmt->execute();
		}
	}
	mysqli_close($conn);
?>
<meta http-equiv="refresh" content="0;url=comment.php" />

위와 같이 쿼리에 값을 직접 넣지 않고 값이 들어갈 자리에 ?를 넣은 다음 bind_param을 이용하여 값을 넣습니다. 앞의 ‘s’는 문자열을 의미합니다. 컬럼이 두 개 이상일 때도 함께 바인딩할 수 있는데 첫 번째와 두 번째가 문자열이고 세 번째가 정수형이면 bind_param('ssi', $str1, $str2, $intg) 식으로 할 수도 있습니다. 그리고 execute 메소드를 실행하면 비로소 바인딩된 쿼리가 실행됩니다.

이렇게 하면 쿼리에 직접 값을 넣는 앞의 방식과 같은 효과를 가져오면서도 차이가 있는데, 다시 [메롱’), (‘메롱] 이렇게 입력해 봅시다. 그리고

SELECT * FROM comments;

다시 서버측 SQL 콘솔에서 이 쿼리를 실행시켜 보면,

+----+--------------------+
| no | comment            |
+----+--------------------+
|  1 | Hello, world!      |
|  2 | 메롱               |
|  3 | 메롱               |
|  4 | 메롱'), ('메롱     |
+----+--------------------+

이렇게 메롱이 두 번 들어가지 않고 입력한 그대로 삽입됨을 알 수 있습니다.

이와 같이 PHP와 MySQL을 연동할 때 바인딩을 통해 SQL 인젝션을 방어할 수 있습니다.

답글 남기기

이메일 주소는 공개되지 않습니다.