×

Cách viết class để bảo vệ SQL Injection trong PHP

SQL Injection là một trong những lỗ hổng bảo mật phổ biến nhất trong các ứng dụng web, có thể dẫn đến việc truy cập trái phép vào cơ sở dữ liệu và dữ liệu nhạy cảm. Để bảo vệ ứng dụng PHP của bạn khỏi các cuộc tấn công SQL Injection, bạn có thể sử dụng một lớp (class) để thực hiện các thao tác truy vấn cơ sở dữ liệu một cách an toàn. Trong bài viết này, chúng ta sẽ tìm hiểu cách viết một lớp PHP để bảo vệ an toàn khỏi các cuộc tấn công SQL Injection.

Khái niệm về SQL Injection

SQL Injection xảy ra khi một kẻ tấn công chèn mã SQL độc hại vào các truy vấn SQL thông qua các đầu vào không được kiểm soát. Điều này cho phép kẻ tấn công thao túng cơ sở dữ liệu, truy vấn thông tin nhạy cảm hoặc thậm chí xóa dữ liệu. Để giảm thiểu nguy cơ này, việc chuẩn hóa và xử lý đầu vào là rất quan trọng.

Tại sao nên sử dụng Prepared Statements

Biện pháp hiệu quả nhất để ngăn chặn SQL Injection là sử dụng Prepared Statements. Khi sử dụng Prepared Statements, bạn có thể phân tách các câu lệnh SQL và dữ liệu đầu vào, giúp bạn tránh việc chạy mã SQL độc hại. Hầu hết các mở rộng PDO và MySQLi của PHP đều hỗ trợ Prepared Statements.

Thiết kế lớp PHP để bảo vệ khỏi SQL Injection

Chúng ta sẽ viết một lớp đơn giản sử dụng PDO cho việc kết nối và thực hiện truy vấn trên cơ sở dữ liệu.

Bước 1: Tạo lớp Database

<?php
class Database {
    private $host;
    private $db_name;
    private $username;
    private $password;
    private $conn;

    public function __construct($host, $db_name, $username, $password) {
        $this->host = $host;
        $this->db_name = $db_name;
        $this->username = $username;
        $this->password = $password;
        $this->connect();
    }

    private function connect() {
        try {
            $this->conn = new PDO("mysql:host=$this->host;dbname=$this->db_name", $this->username, $this->password);
            $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException $exception) {
            echo "Connection error: " . $exception->getMessage();
        }
    }

    public function prepare($sql) {
        return $this->conn->prepare($sql);
    }

    public function execute($stmt) {
        return $stmt->execute();
    }

    public function fetchAll($stmt) {
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function fetch($stmt) {
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function lastInsertId() {
        return $this->conn->lastInsertId();
    }
}
?>

Bước 2: Sử dụng lớp Database

Giờ đây, chúng ta có thể sử dụng lớp Database mà chúng ta đã tạo để thực hiện các truy vấn mà không lo ngại về SQL Injection.

<?php
// Kết nối đến cơ sở dữ liệu
$db = new Database('localhost', 'my_database', 'username', 'password');

// Thực hiện truy vấn an toàn
$username = 'user_input'; // Đây là dữ liệu người dùng nhập vào
$sql = "SELECT * FROM users WHERE username = :username";
$stmt = $db->prepare($sql);

// Liên kết biến
$stmt->bindParam(':username', $username);

// Thực hiện truy vấn
$db->execute($stmt);

// Lấy kết quả
$results = $db->fetchAll($stmt);
foreach ($results as $row) {
    echo $row['username'];
}
?>

Bước 3: Bảo vệ dữ liệu đầu vào

Mặc dù việc sử dụng Prepared Statements là cách tốt nhất để bảo vệ ứng dụng của bạn khỏi SQL Injection, nhưng bạn cũng nên thực hiện việc kiểm tra và làm sạch dữ liệu đầu vào.

function cleanInput($data) {
    // Xóa khoảng trắng
    $data = trim($data);
    // Xóa ký tự đặc biệt
    $data = stripslashes($data);
    $data = htmlspecialchars($data);
    return $data;
}

// Sử dụng hàm cleanInput
$username = cleanInput($_POST['username']);

Bước 4: Kiểm tra lỗi

Nếu bạn muốn theo dõi các lỗi, hãy cân nhắc thêm một phương thức trong lớp Database để ghi lại các lỗi truy vấn.

public function error($stmt) {
    return $stmt->errorInfo();
}

Bước 5: Định nghĩa các phương thức CRUD

Để làm cho lớp Database của bạn trở nên hoàn chỉnh, bạn có thể định nghĩa các phương thức cho các hoạt động CRUD (Create, Read, Update, Delete):

public function insert($sql, $params) {
    $stmt = $this->prepare($sql);
    foreach ($params as $key => $value) {
        $stmt->bindValue($key, $value);
    }
    if ($this->execute($stmt)) {
        return $this->lastInsertId();
    }
    return false;
}

public function update($sql, $params) {
    $stmt = $this->prepare($sql);
    foreach ($params as $key => $value) {
        $stmt->bindValue($key, $value);
    }
    return $this->execute($stmt);
}

public function delete($sql, $params) {
    $stmt = $this->prepare($sql);
    foreach ($params as $key => $value) {
        $stmt->bindValue($key, $value);
    }
    return $this->execute($stmt);
}

Bước 6: Triển khai các phương thức CRUD

Để sử dụng các phương thức CRUD đã được định nghĩa, bạn có thể làm theo ví dụ sau:

// Thực hiện chèn dữ liệu
$sqlInsert = "INSERT INTO users (username, password) VALUES (:username, :password)";
$params = [':username' => $cleanUsername, ':password' => password_hash($password, PASSWORD_BCRYPT)];
$newUserId = $db->insert($sqlInsert, $params);

// Thực hiện cập nhật dữ liệu
$sqlUpdate = "UPDATE users SET password = :password WHERE username = :username";
$params = [':username' => $cleanUsername, ':password' => password_hash($newPassword, PASSWORD_BCRYPT)];
$db->update($sqlUpdate, $params);

// Thực hiện xóa dữ liệu
$sqlDelete = "DELETE FROM users WHERE username = :username";
$params = [':username' => $cleanUsername];
$db->delete($sqlDelete, $params);

Kết luận

Việc bảo vệ ứng dụng PHP khỏi SQL Injection là một phần không thể thiếu trong phát triển ứng dụng web. Sử dụng một lớp như Database giúp bạn thực hiện các truy vấn một cách an toàn hơn mà không cần phải lo lắng về việc lộ thông tin nhạy cảm. Hãy luôn kiểm tra và làm sạch dữ liệu đầu vào của người dùng, và sử dụng Prepared Statements để bảo vệ cơ sở dữ liệu của bạn. Bằng cách làm theo các nguyên tắc trên, bạn sẽ tạo ra một ứng dụng an toàn và bảo mật hơn.

Comments