[SQLi] SQL injection 대응 방안

CHIKA·2024년 7월 17일

SQL injection 대응 방안

1. prepared Statement

📌 Prepared Statement 란?
SQL 질의문을 사전에 컴파일하고 실행 시 변수 값을 바인딩하여 실행하는 방식으로, SQLi 공격을 방지할 수 있다.


MySQL로 Prepared Statement 실습

1) 실습 DB 만들기

 CREATE DATABASE SQLi_ex;
 USE SQLi_ex;
 CREATE TABLE users (id VARCHAR(50) PRIMARY KEY, pw VARCHAR(255) NOT NULL);
 INSERT INTO users (id , pw) values ('chika','puka');

로그인 과정에서 일어나는 SQLi 재현을 위해 id = chika , pw = puka 데이터를 넣어주었다.

2) 폼 만들기

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="login.php" method = "post">
        <input type="text" placeholder="아이디를 입력하세요." name="id"><br>
        <input type="text" placeholder="비밀번호를 입력하세요." name="pw"><br>
        <input type="submit" value="submit">
    </form>
</body>
</html>

간단하게 아이디, 비밀번호 입력받을 폼을 만들어 주었다.

3) login.php 만들기

<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "SQLi_ex";


// 데이터베이스에 연결
$conn = new mysqli($servername, $username, $password, $dbname);

// 연결 확인
if ($conn->connect_error) {
    die("연결 실패: " . $conn->connect_error);
}

$id = $_POST['id'];
$pw = $_POST['pw'];

$sql = "SELECT id, pw FROM users WHERE id = '$id' and pw = '$pw' ";
$result = $conn->query($sql);
// 결과 확인 후 로그인 처리
if ($result->num_rows > 0) {
    echo "로그인 되었습니다.";
} else {
    echo "로그인에 실패하였습니다.";
}
// 데이터베이스 연결 종료
$conn->close();
?>

prepared Statement 와 비교를 위해 만들었다.
SQL query : SELECT id, pw FROM users WHERE id = '$id' and pw = '$pw' 이니까
id에 chika' or '1'='1 을 입력하면
SELECT id, pw FROM users WHERE id = 'chika' or '1'='1' and pw = '$pw'가 되어 아무 비밀번호나 넣어도 로그인이 가능할 것이다.


로그인 된다. prepared statement를 적용해보자.

4) pStmt.php 만들기

<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "SQLi_ex";


// 데이터베이스에 연결
$conn = new mysqli($servername, $username, $password, $dbname);

// 연결 확인
if ($conn->connect_error) {
    die("연결 실패: " . $conn->connect_error);
}

$id = $_POST['id'];
$pw = $_POST['pw'];

$sql = "SELECT id, pw FROM users WHERE id = ? and pw = ?";

// Prepared Statement 생성
$stmt = $conn->prepare($sql);

// 변수 값 바인딩 (s: string, i: integer, d: double, b: blob)
$stmt->bind_param("ss",$id,$pw);

// 쿼리 실행
$stmt->execute();

// 결과 가져오기
$result = $stmt->get_result();

// 결과 처리
if ($row = $result->fetch_assoc()) {
    echo "회원정보가 일치합니다."."<br>"."ID: " . $row['id'] . " - pw: " . $row['pw'] . "<br>";
} else {
    echo "회원정보가 일치하지 않습니다.";
}

// Statement 및 연결 종료
$stmt->close();
$conn->close();
?>

동일한 SQL query를 사용한다.
똑같이 아이디에 chika' or '1'='1 과 아무 비밀번호를 넣어보자.

로그인이 안된다. 혹시 변수바인딩에 ? 를 써서 그럴까?
수정해서 넣어보자.

로그인이 안된다. SQL 쿼리를 먼저 컴파일 해놓고 변수를 바인딩 하기 때문에 데이터가 분리되어 함께 실행되지 않기 때문이다.


하지만 prepared statement도 완벽한 해결책이 될 수 없다.
왜냐면 prepared statement가 적용이 안되는 곳이 있기 때문이다.
order by 절은 prepared Statement를 적용할 수 없다.
이를 해결하기 위해서 화이트 리스트 기반의 필터링이 필요하다.

2. 화이트 리스트 기반 필터링.

order by 절 뒤에 조작된 파라미터가 오는 것을 막기 위해 화이트 리스트 기반 필터링을 사용할 수 있다.

허용할 컬럼명의 화이트 리스트를 만들어 놓고
사용자의 입력 값이 화이트 리스트에 있는지 확인하여 존재하지 않으면 설정된 값을 사용하는 등의 방법을 사용할 수 있다.

// 허용된 컬럼명의 화이트리스트
$allowed_columns = ['id', 'title', 'date'];

// 사용자 입력 값
$user_sort_column = $_GET['sort'];

// 입력 값이 화이트리스트에 있는지 확인
if (in_array($user_sort_column, $allowed_columns)) {
    // 화이트리스트에 있는 값이면, 안전한 값으로 간주하고 SQL 쿼리 작성
    $sort_column = $user_sort_column;
} else {
    // 화이트리스트에 없는 값이면, 기본 값 사용
    $sort_column = 'name';
}

참고자료
https://fis.kr//ko/major_biz/cyber_safety_oper/attack_info/security_news?articleSeq=2588

0개의 댓글