728x90
PHP에서 SQL 인젝션(SQL Injection)을 예방하는 방법은 웹 애플리케이션 보안의 핵심 요소입니다.
1. SQL 인젝션이란?
SQL 인젝션은 사용자가 입력한 데이터가 SQL 문에 삽입되면서 원래 의도하지 않은 SQL 명령이 실행되는 보안 취약점입니다.
예시 공격
$_POST['id'] = "'; DROP TABLE users; --";
$sql = "SELECT * FROM users WHERE id = '{$_POST['id']}'";
// 실행 시: SELECT * FROM users WHERE id = ''; DROP TABLE users; --';
2. 과거 방식: magic_quotes_gpc
와 addslashes()
🔸 magic_quotes_gpc
- PHP 설정 중 하나로
$_GET
,$_POST
,$_COOKIE
의 입력값에 자동으로addslashes()
를 적용. - 보안용으로 설계되었으나 문법에 정확히 맞지 않고, 신뢰할 수 없음.
\
로 escape 하긴 하지만, SQL 문법은 RDBMS마다 다르고 escape 처리도 다름.- 취약점 예시: 세미콜론
;
은 escape되지 않아 SQL 명령 분리 가능.
주의: PHP 5.4.0부터 완전히 제거됨. 더 이상 사용 불가.
✅ 3. 문자열 escape 처리: *_escape_string()
🔸 mysql_escape_string(), mysqli_real_escape_string(), pg_escape_string() 등
- 데이터베이스별로 제공되는 escape 함수.
- 문자열 내 특수 문자(예:
'
,"
,\
,NULL
)를 안전하게 변환해 SQL 인젝션을 방지. - 사용 예시:
$conn = mysqli_connect(...); $escaped = mysqli_real_escape_string($conn, $_POST['username']); $query = "INSERT INTO users (username) VALUES ('$escaped')";
주의사항:
- 쿼리 문자열로 직접 데이터를 삽입하는 방식은 여전히 위험.
- escape 후에도 SQL 문법 오류로 인한 취약점 발생 가능성 있음.
- select 결과값을 다시 DB에 쓰는 경우, 재escape 필요.
✅ 4. 근본적인 해결책: Prepared Statements (준비된 쿼리)
🔸 PDO (PHP Data Objects)
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $db->prepare('INSERT INTO users (username, email) VALUES (:username, :email)');
$stmt->execute([
':username' => $_POST['username'],
':email' => $_POST['email']
]);
✅ 장점
- 쿼리와 데이터를 명확히 분리: SQL 구문과 입력값은 별도로 처리됨.
- 자동으로 데이터 형식에 따라 처리: 문자열은 자동으로 따옴표 처리됨.
- 입력값에 escape 불필요: 내부적으로 안전하게 처리됨.
- SQL 인젝션 방지 완전 보장: 실행 단계에서 SQL이 변경되지 않음.
🔸 bindParam() 방식 (참조 변수 사용)
$stmt = $db->prepare("INSERT INTO users (username) VALUES (:username)");
$stmt->bindParam(':username', $username);
$username = $_POST['username'];
$stmt->execute();
✅ 5. 각 데이터베이스별 Prepared Statement
- MySQLi
$conn = new mysqli(...); $stmt = $conn->prepare("INSERT INTO users (username) VALUES (?)"); $stmt->bind_param("s", $_POST['username']); $stmt->execute();
- PostgreSQL (pg_query_params 사용)
$conn = pg_connect(...); pg_query_params($conn, 'INSERT INTO users (username) VALUES ($1)', [$_POST['username']]);
✅ 6. 기타 보안 팁
- 입력값 검증(Validation): 숫자, 이메일, 문자열 길이 등을 사전에 체크.
- 화이트리스트 기반 필터링: 허용된 값만 통과시키는 방식.
- 출력 시에는 반드시 이스케이프: XSS 방지 (
htmlspecialchars()
등). - ORM 사용 고려: Laravel Eloquent, Doctrine 등은 내부적으로 prepared statement 사용.
✅ 결론 및 권장 사항
방식 | 보안성 | 사용 권장 | 비고 |
---|---|---|---|
magic_quotes_gpc |
❌ 낮음 | ❌ 비권장 | PHP 5.4 이후 제거 |
addslashes() , *_escape_string() |
⚠️ 보통 | ⚠️ 제한적 사용 | 실수 많음 |
Prepared Statement (PDO 등) | ✅ 높음 | ✅ 강력 추천 | 최선의 방법 |
요약:
항상 prepared statement를 사용하라. escape는 보완책일 뿐 근본 해결은 아니다.
입력값을 직접 SQL에 삽입하지 말고, 쿼리와 데이터를 분리하여 SQL 인젝션을 방지하라.
아래는 PHP에서 SQL 인젝션을 직접 테스트해볼 수 있는 시나리오와 실습 환경 구성 방법을 안내합니다. 취약한 예제부터 안전한 코드까지 단계적으로 제공하며, 로컬 또는 테스트 서버에서 실행할 수 있습니다.
✅ 1. 테스트 환경 구성
🧰 준비물
- PHP + MySQL(MariaDB)
- 로컬 서버 (XAMPP, MAMP, Docker 등)
- phpMyAdmin 또는 MySQL CLI
- 테스트용 DB 및 테이블
📦 데이터베이스 생성 및 테이블 만들기
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100),
password VARCHAR(100)
);
🚨 2. 취약한 코드 예제 (SQL Injection 가능)
filename: login_vulnerable.php
<?php
$conn = new mysqli("localhost", "root", "", "testdb");
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
echo "<p><b>Query:</b> $query</p>";
$result = $conn->query($query);
if ($result && $result->num_rows > 0) {
echo "✅ Login success!";
} else {
echo "❌ Invalid login.";
}
?>
💥 공격 시나리오
- users 테이블에 유저를 삽입:
INSERT INTO users (username, password) VALUES ('admin', 'pass123');
- 로그인 폼에 입력:
username: admin' -- password: anything
결과:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'anything'
--
주석 처리로 password 조건 무시됨 → 로그인 우회 성공
✅ 3. 안전한 코드 (Prepared Statement 사용)
filename: login_secure.php
<?php
$conn = new mysqli("localhost", "root", "", "testdb");
$username = $_POST['username'];
$password = $_POST['password'];
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();
if ($result && $result->num_rows > 0) {
echo "✅ Login success!";
} else {
echo "❌ Invalid login.";
}
?>
✅ 테스트 시나리오
동일한 공격 시도:
username: admin' --
password: anything
결과: 쿼리에 주석문 포함 불가 → SQL 인젝션 실패
✅ 4. 비교 실습 요약
항목 | 취약한 코드 | 안전한 코드 |
---|---|---|
SQL 인젝션 가능 여부 | ✅ 가능 | ❌ 차단 |
입력값 검증 | ❌ 없음 | ⚠️ (개별적으로 추가 가능) |
escape 필요 | ✅ 수동 필요 | ❌ 자동 처리됨 |
권장 여부 | ❌ 금지 | ✅ 강력 권장 |
✅ 5. 추가 실습 아이디어
- POST 대신 GET으로 바꾸고 URL 인젝션 실습
- 다양한 인젝션 페이로드 테스트
' OR 1=1 --
' UNION SELECT null, version() --
- PDO 방식으로도 동일한 시나리오 실습
✅ 참고용 간단 로그인 HTML 폼
<form method="post" action="login_vulnerable.php">
<input type="text" name="username" placeholder="Username">
<input type="text" name="password" placeholder="Password">
<button type="submit">Login</button>
</form>
주의: 이 코드는 교육 목적이며, 실제 서비스에서는 절대 취약한 코드를 사용하지 마세요.
필요하면 Docker 또는 VM 내부에서 실습을 진행하시기 바랍니다.
728x90
그리드형(광고전용)
댓글