
PHP 강의
>PHP - 중급
📚 PHP 중급 - 1주차: 데이터베이스 기초 및 MySQL 연동 (PDO) - 04 SQL Injection 방지를 위한 Prepared Statement
![]() |
평점 | 10.0 | 라이센스 | free |
---|---|---|---|---|
사용자평점 | 10.0 | 운영체제 | ||
다운로드 | 1 | 파일크기 | 0 | |
제작사 | LUZENSOFT | 등록일 | 2025-07-12 12:38:25 | |
조회수 | 25 |
📚 PHP 중급 - 1주차: 데이터베이스 기초 및 MySQL 연동 (PDO) - 04 SQL Injection 방지를 위한 Prepared Statement
#SQL 인젝션(Injection)이란 무엇인가?
안녕하세요! #PHP 중급 과정의 네 번째 시간입니다. 지난 시간에는 #PDO를 이용한 #데이터베이스 #연결 방법에 대해 자세히 알아봤죠. 오늘은 웹 #보안에서 가장 중요하고 기본적인 부분인 #SQL #인젝션에 대해 이해하고, 이를 방지하기 위한 핵심 기술인 #Prepared #Statement에 대해 학습할 거예요.
SQL 인젝션은 #웹 #애플리케이션의 #보안 #취약점을 이용한 공격 기법 중 하나입니다. 공격자는 웹 페이지의 입력 필드(예: 로그인 폼, 검색창)에 #악의적인 #SQL #코드 조각을 삽입하여 데이터베이스를 조작하거나, #민감한 #정보를 #탈취하거나, 심지어 #데이터를 #파괴할 수도 있습니다.
예를 들어, 사용자 이름과 비밀번호를 입력받아 로그인하는 페이지가 있다고 가정해 봅시다. 만약 개발자가 사용자 입력을 제대로 검증하지 않고 SQL 쿼리에 그대로 삽입한다면 다음과 같은 문제가 발생할 수 있습니다.
원래 의도된 쿼리: SELECT * FROM users WHERE username = '사용자입력_아이디' AND password = '사용자입력_비밀번호'
공격자가 ' OR '1'='1' --'를 사용자입력_비밀번호에 삽입할 경우: SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1' --'
위 쿼리에서 --
는 SQL에서 주석을 의미합니다. 이렇게 되면 password = '' OR '1'='1'
부분은 항상 참(True)이 되어, 공격자는 비밀번호를 몰라도 admin
계정으로 로그인할 수 있게 됩니다. 이는 매우 위험한 보안 구멍이 됩니다.
Prepared Statement가 SQL 인젝션을 막는 원리
#준비된 #구문(Prepared Statement)은 SQL 인젝션 공격을 방지하는 가장 효과적인 방법입니다. PDO는 Prepared Statement 기능을 강력하게 지원합니다.
Prepared Statement의 작동 원리는 다음과 같습니다.
쿼리 구조와 데이터 분리: SQL 쿼리의 구조(템플릿)와 사용자가 입력한 데이터(매개변수)를 분리하여 데이터베이스에 전달합니다.
예:
SELECT * FROM users WHERE username = :username AND password = :password
여기서
:username
과:password
는 #플레이스홀더(placeholder)입니다.
쿼리 컴파일: 데이터베이스는 플레이스홀더가 포함된 쿼리 구조를 먼저 받아서 컴파일(준비)합니다. 이 단계에서는 아직 사용자 데이터가 포함되지 않았으므로, 데이터베이스는 쿼리 구조만 파악하고 최적화합니다.
데이터 바인딩 및 실행: 나중에 사용자 데이터를 데이터베이스에 별도로 전송하고, 데이터베이스는 이 데이터를 미리 컴파일된 쿼리 구조에 '바인딩'하여 실행합니다. 이때 데이터베이스는 사용자 데이터를 순수한 데이터 값으로만 인식하며, 어떤 경우에도 실행 가능한 SQL 코드로 해석하지 않습니다.
이러한 분리 덕분에, 공격자가 입력 필드에 아무리 악의적인 SQL 코드를 삽입해도, 데이터베이스는 이를 단순히 텍스트 값으로만 인식하고 SQL 명령어로 실행하지 않게 됩니다.
#PDO를 이용한 Prepared Statement 사용법
PDO에서 Prepared Statement를 사용하는 방법은 크게 두 단계로 나눌 수 있습니다: #준비(Prepare)와 #실행(Execute).
1. #준비 (Prepare)
PDO
객체의 prepare()
메서드를 사용하여 SQL 쿼리 템플릿을 데이터베이스에 보냅니다. prepare()
메서드는 PDOStatement
객체를 반환합니다. 이 객체가 실제 쿼리를 실행하는 데 사용됩니다.
플레이스홀더는 두 가지 방식 중 하나를 사용할 수 있습니다:
이름 있는 플레이스홀더 (Named Placeholders):
:컬럼명
또는:별칭
과 같이 콜론(:
) 뒤에 이름을 붙입니다. 가독성이 좋고, 매개변수가 많을 때 유용합니다. (권장)PHP
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
물음표 플레이스홀더 (Question Mark Placeholders):
?
를 사용합니다. 매개변수 순서에 의존하므로 순서가 꼬이지 않도록 주의해야 합니다.PHP
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
2. #데이터 바인딩 (Bind Data)
PDOStatement
객체에는 데이터를 플레이스홀더에 바인딩하는 여러 메서드가 있습니다.
bindValue(placeholder, value, [data_type])
:매개변수 값을 플레이스홀더에 바인딩합니다. 한 번 바인딩하면 값이 고정됩니다.
data_type
은PDO::PARAM_STR
(문자열),PDO::PARAM_INT
(정수) 등으로 명시할 수 있으며, 생략 시 PDO가 자동으로 추론하지만 명시하는 것이 좋습니다.
PHP
// 이름 있는 플레이스홀더의 경우 $stmt->bindValue(':username', $inputUsername, PDO::PARAM_STR); $stmt->bindValue(':password', $inputPassword, PDO::PARAM_STR); // 물음표 플레이스홀더의 경우 (1부터 시작하는 인덱스) $stmt->bindValue(1, $inputUsername, PDO::PARAM_STR); $stmt->bindValue(2, $inputPassword, PDO::PARAM_STR);
bindParam(placeholder, variable, [data_type])
:매개변수를 변수 참조에 바인딩합니다.
execute()
호출 시점에 변수의 현재 값을 사용합니다. 동일한 쿼리를 여러 번 실행할 때 효율적입니다.
PHP
$user = 'some_user'; $pass = 'some_pass'; $stmt->bindParam(':username', $user, PDO::PARAM_STR); $stmt->bindParam(':password', $pass, PDO::PARAM_STR); $stmt->execute(); // $user와 $pass의 현재 값이 사용됨 $user = 'another_user'; $pass = 'another_pass'; $stmt->execute(); // $user와 $pass의 새로운 값이 사용됨
3. #실행 (Execute)
모든 매개변수를 바인딩한 후, execute()
메서드를 호출하여 준비된 쿼리를 실행합니다.
execute()
: 바인딩된 값을 사용하여 쿼리를 실행합니다.PHP
$stmt->execute();
execute(array $input_parameters)
:bindValue()
나bindParam()
을 사용하지 않고,execute()
메서드의 인자로 배열을 전달하여 한 번에 모든 플레이스홀더에 값을 바인딩하고 실행할 수도 있습니다. 이 방법이 가장 간결하고 많이 사용됩니다.PHP
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password"); $stmt->execute([ ':username' => $inputUsername, ':password' => $inputPassword ]);
물음표 플레이스홀더의 경우, 배열의 순서대로 바인딩됩니다.
PHP
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$inputUsername, $inputPassword]);
#Prepared Statement 실습 예제
데이터베이스 연결 코드는 이전 포스팅을 참고하며, 로그인 기능을 예시로 Prepared Statement를 적용해 봅시다.
PHP
<?php
// DB_Connect.php (이전 포스팅에서 만든 DB 연결 코드를 포함한다고 가정)
require 'DB_Connect.php'; // $pdo 객체가 여기에 정의되어 있다고 가정합니다.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
echo "사용자 이름과 비밀번호를 모두 입력해주세요.". "<br>";
} else {
try {
// 1. 쿼리 준비 (플레이스홀더 사용)
// SQL 인젝션 방지를 위해 사용자 입력을 직접 쿼리에 넣지 않습니다.
$sql = "SELECT id, username FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
// 2. 데이터 바인딩 및 실행
// execute() 메서드에 배열로 매개변수 전달
$stmt->execute([
':username' => $username,
':password' => $password // 실제 앱에서는 비밀번호를 해싱해서 비교해야 함!
]);
// 3. 결과 가져오기
$user = $stmt->fetch();
if ($user) {
echo "로그인 성공! 환영합니다, " . htmlspecialchars($user['username']) . "!". "<br>";
// 세션 시작 및 사용자 정보 저장 등의 로그인 처리
} else {
echo "로그인 실패: 사용자 이름 또는 비밀번호가 올바르지 않습니다.". "<br>";
}
} catch (PDOException $e) {
echo "데이터베이스 오류: " . $e->getMessage(). "<br>";
// 실제 서비스에서는 오류를 로깅하고 사용자에게는 일반적인 메시지를 보여줍니다.
}
}
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>로그인 예제</title>
</head>
<body>
<h2>로그인</h2>
<form method="POST" action="">
<label for="username">사용자 이름:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="password">비밀번호:</label><br>
<input type="password" id="password" name="password" required><br><br>
<input type="submit" value="로그인">
</form>
</body>
</html>
참고: 위 예제는 보안 강화를 위해 실제 비밀번호를 해싱하여 데이터베이스에 저장하고, 로그인 시에도 입력된 비밀번호를 해싱하여 비교하는 과정이 추가되어야 합니다. #password_hash()
와 #password_verify()
함수를 사용하는 것이 표준입니다.
결론 및 다음 주차 예고
이번 포스팅에서는 #SQL #인젝션의 위험성을 명확히 이해하고, 이를 방지하기 위한 필수적인 기술인 #Prepared #Statement의 원리와 #PDO에서의 사용법을 익혔습니다. #쿼리 #구조와 #데이터를 분리하여 처리함으로써, 사용자 입력이 데이터베이스 명령어로 오작동하는 것을 원천적으로 차단할 수 있음을 배웠습니다.
Prepared Statement는 PDO를 사용하는 가장 큰 이유 중 하나이며, 여러분의 웹 애플리케이션을 #보안 위협으로부터 지키는 첫걸음입니다. 항상 사용자 입력을 받는 모든 SQL 쿼리에 Prepared Statement를 적용하는 습관을 들이세요.