사용자가 입력한 값을 그대로 SQL 문자열에 이어 붙이면 문제가 생깁니다. 이름에 O'Brien 처럼 작은따옴표가 하나만 들어가도 쿼리 문법이 깨지고, 악의적으로 ' OR '1'='1 같은 값을 넣으면 의도와 다른 쿼리가 실행됩니다. 흔히 말하는 SQL 인젝션입니다. 그래서 쿼리에 들어갈 문자열은 반드시 처리를 거쳐야 하고, PHP의 mysqli 확장에서는 그 용도로 mysqli_real_escape_string 을 제공합니다.
mysqli_real_escape_string($dbc, 처리할 문자열);
첫 번째 인자는 mysqli_connect 로 얻은 연결 객체, 두 번째는 처리할 문자열입니다. 작은따옴표, 큰따옴표, 백슬래시, NUL, 줄바꿈 같은 문자 앞에 백슬래시를 붙인 문자열을 돌려줍니다.
실제 쓰는 모양
$name = mysqli_real_escape_string($dbc, $_POST['name']);
$sql = "SELECT * FROM member WHERE name = '$name'";
$result = mysqli_query($dbc, $sql);
여기서 중요한 건 이스케이프한 값을 반드시 따옴표 안에 넣어야 한다는 점입니다. 이 함수는 문자열 리터럴 안에서 따옴표를 빠져나가지 못하게 막아 줄 뿐이라, WHERE id = $id 처럼 따옴표 없이 숫자 자리에 넣으면 보호가 되지 않습니다. 숫자 값은 (int) 로 형변환하는 게 맞습니다.
연결 객체를 요구하는 이유도 있습니다. 어떤 문자를 어떻게 처리할지는 연결의 문자셋에 따라 달라지기 때문입니다. 그래서 연결 직후 mysqli_set_charset($dbc, 'utf8mb4') 로 문자셋을 지정해 두는 게 좋습니다. SET NAMES 쿼리로 바꾸면 함수 쪽이 그 변경을 모르니 mysqli_set_charset 을 쓰세요.
가능하면 prepared statement
새로 짜는 코드라면 이스케이프보다 prepared statement를 쓰는 게 기본입니다. 값과 쿼리 구조가 따로 전달되니 따옴표를 빼먹는 실수가 원천적으로 안 생깁니다.
$stmt = mysqli_prepare($dbc, "SELECT * FROM member WHERE name = ?");
mysqli_stmt_bind_param($stmt, 's', $_POST['name']);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
mysqli_real_escape_string 은 이미 문자열 연결로 짜인 오래된 코드를 급하게 막아야 할 때, 또는 동적으로 만든 IN 목록처럼 prepared statement로 옮기기 번거로운 곳에 쓰는 정도로 생각하면 됩니다. 테이블명이나 컬럼명처럼 식별자 자리에는 이 함수도 prepared statement도 소용이 없으니, 그런 값은 허용 목록(화이트리스트)과 비교해서 걸러야 합니다.
IN 목록도 prepared statement로 바꿀 수 있다
IN 목록이 번거롭다고 했지만, 값 개수만큼 ? 를 만들어 붙이면 prepared statement로도 어렵지 않게 바꿀 수 있습니다.
$names = $_POST['names']; // 문자열 배열
$marks = implode(',', array_fill(0, count($names), '?'));
$stmt = mysqli_prepare($dbc, "SELECT * FROM member WHERE name IN ($marks)");
mysqli_stmt_bind_param($stmt, str_repeat('s', count($names)), ...$names);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
쿼리에 이어 붙는 건 ?,?,? 같은 자리표시자뿐이고 실제 값은 따로 넘어가니 안전합니다. 배열이 비어 있으면 IN () 이 되어 문법 오류가 나니, 빈 배열일 때는 쿼리를 아예 실행하지 않도록 앞에서 걸러 두세요.
LIKE 검색에서 빠지는 부분
mysqli_real_escape_string 은 % 와 _ 를 처리하지 않습니다. 둘 다 SQL 문법을 깨는 문자는 아니라서인데, LIKE 안에서는 "아무 글자" 라는 뜻이 됩니다. 사용자가 검색어로 100% 를 넣으면 100 으로 시작하는 모든 값이 걸리는 식이죠. 문자 그대로 찾게 하려면 LIKE 패턴을 만들 때 이 두 문자 앞에 백슬래시를 붙여야 합니다.
$kw = '%' . addcslashes($_GET['q'], '%_\\') . '%';
$stmt = mysqli_prepare($dbc, "SELECT * FROM member WHERE name LIKE ?");
mysqli_stmt_bind_param($stmt, 's', $kw);
오래된 코드를 손볼 때는 이중 이스케이프도 조심해야 합니다. 옛 PHP의 magic_quotes_gpc 시절에 짠 코드는 입력값에 이미 백슬래시가 붙어 있다고 가정하고 stripslashes 를 부르거나, 반대로 addslashes 를 한 번 더 거는 경우가 있습니다. 이 설정은 PHP 5.4에서 없어졌으니, 그런 코드가 남아 있다면 데이터에 O\'Brien 처럼 백슬래시가 그대로 저장되고 있지 않은지 확인해 보세요.
그리고 화면에 출력할 때의 처리(htmlspecialchars)와 DB에 넣을 때의 처리는 서로 다른 문제입니다. DB에 넣기 전에 HTML 이스케이프를 하거나, 출력할 때 SQL 이스케이프를 하는 식으로 섞어 쓰면 데이터에 백슬래시나 & 가 쌓이는 일이 생깁니다.