أساسيات PHP
العبارات المحضرة ومنع حقن SQL
فهم حقن SQL
حقن SQL هو أحد أخطر نقاط ضعف تطبيقات الويب. يحدث عندما يتمكن المهاجم من إدراج كود SQL ضار في استعلاماتك من خلال مدخلات المستخدم.
مثال على كود غير آمن
<?php // لا تفعل هذا أبدًا! $username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = $conn->query($sql); ?>
خطر! إذا أدخل المهاجم
هذا يُرجع جميع المستخدمين لأن '1'='1' صحيح دائمًا!
' OR '1'='1 كاسم مستخدم، يصبح الاستعلام:SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ''هذا يُرجع جميع المستخدمين لأن '1'='1' صحيح دائمًا!
كيف يعمل حقن SQL
يمكن للمهاجمين استغلال حقن SQL من أجل:
- تجاوز المصادقة: تسجيل الدخول بدون بيانات اعتماد صالحة
- سرقة البيانات: الوصول إلى معلومات حساسة من قاعدة البيانات
- تعديل البيانات: تحديث أو حذف السجلات
- تنفيذ الأوامر: تشغيل أوامر النظام على الخادم
- حذف الجداول: حذف قواعد بيانات كاملة
أمثلة شائعة لحقن SQL
// تجاوز المصادقة Username: admin'-- Password: anything // إرجاع جميع المستخدمين Username: ' OR '1'='1 Password: ' OR '1'='1 // سرقة البيانات عبر UNION Username: ' UNION SELECT credit_card FROM payments-- Password: anything // حذف الجداول Username: '; DROP TABLE users-- Password: anything
العبارات المحضرة: الحل
تفصل العبارات المحضرة كود SQL عن البيانات، مما يجعل حقن SQL مستحيلاً. تعمل في خطوتين:
- التحضير: تحديد بنية SQL مع العناصر النائبة
- الربط والتنفيذ: ربط المعلمات وتنفيذ الاستعلام
بناء جملة العبارة المحضرة الأساسية
<?php
// الخطوة 1: تحضير العبارة مع العناصر النائبة (?)
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND email = ?");
// الخطوة 2: ربط المعلمات (s = سلسلة نصية، i = عدد صحيح، d = مزدوج، b = ثنائي)
$stmt->bind_param("ss", $username, $email);
// الخطوة 3: تعيين قيم المعلمات
$username = "johndoe";
$email = "john@example.com";
// الخطوة 4: التنفيذ
$stmt->execute();
// الخطوة 5: الحصول على النتائج
$result = $stmt->get_result();
$user = $result->fetch_assoc();
// الخطوة 6: إغلاق العبارة
$stmt->close();
?>
محددات نوع المعلمة
| النوع | الوصف | مثال |
|---|---|---|
i |
عدد صحيح | 42, -10, 0 |
d |
مزدوج/عشري | 3.14, -0.5 |
s |
سلسلة نصية | 'hello', 'user@email.com' |
b |
ثنائي (blob) | بيانات الصور، الملفات |
نصيحة: عند الشك، استخدم 's' (سلسلة نصية) لمعظم القيم. سيتعامل MySQL مع تحويل النوع.
SELECT مع العبارات المحضرة
<?php
require_once 'database.php';
// مدخلات المستخدم (من النموذج)
$search_username = $_POST['username'];
// تحضير العبارة
$stmt = $conn->prepare("SELECT id, username, email, full_name FROM users WHERE username = ?");
// ربط المعلمة
$stmt->bind_param("s", $search_username);
// التنفيذ
$stmt->execute();
// الحصول على النتيجة
$result = $stmt->get_result();
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
echo "المستخدم: " . $row['username'] . " (" . $row['email'] . ")<br>";
}
} else {
echo "لم يتم العثور على مستخدم";
}
$stmt->close();
?>
INSERT مع العبارات المحضرة
<?php
require_once 'database.php';
// مدخلات المستخدم
$username = $_POST['username'];
$email = $_POST['email'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$full_name = $_POST['full_name'];
// تحضير العبارة
$stmt = $conn->prepare("INSERT INTO users (username, email, password, full_name) VALUES (?, ?, ?, ?)");
// ربط المعلمات (4 سلاسل نصية)
$stmt->bind_param("ssss", $username, $email, $password, $full_name);
// التنفيذ
if ($stmt->execute()) {
echo "تم إنشاء المستخدم بنجاح<br>";
echo "معرف المستخدم الجديد: " . $stmt->insert_id;
} else {
echo "خطأ: " . $stmt->error;
}
$stmt->close();
?>
UPDATE مع العبارات المحضرة
<?php
require_once 'database.php';
// مدخلات المستخدم
$new_email = $_POST['email'];
$new_full_name = $_POST['full_name'];
$user_id = $_POST['user_id'];
// تحضير العبارة
$stmt = $conn->prepare("UPDATE users SET email = ?, full_name = ? WHERE id = ?");
// ربط المعلمات (سلسلتان نصيتان، عدد صحيح واحد)
$stmt->bind_param("ssi", $new_email, $new_full_name, $user_id);
// التنفيذ
if ($stmt->execute()) {
echo "تم تحديث المستخدم بنجاح<br>";
echo "الصفوف المتأثرة: " . $stmt->affected_rows;
} else {
echo "خطأ: " . $stmt->error;
}
$stmt->close();
?>
DELETE مع العبارات المحضرة
<?php
require_once 'database.php';
// مدخلات المستخدم
$user_id = $_POST['user_id'];
// تحضير العبارة
$stmt = $conn->prepare("DELETE FROM users WHERE id = ?");
// ربط المعلمة
$stmt->bind_param("i", $user_id);
// التنفيذ
if ($stmt->execute()) {
if ($stmt->affected_rows > 0) {
echo "تم حذف المستخدم بنجاح";
} else {
echo "لم يتم العثور على مستخدم بهذا المعرف";
}
} else {
echo "خطأ: " . $stmt->error;
}
$stmt->close();
?>
مثال معلمات متعددة
<?php
require_once 'database.php';
// البحث عن المستخدمين بمعايير متعددة
$search_name = $_GET['name'];
$min_age = $_GET['min_age'];
$max_age = $_GET['max_age'];
$city = $_GET['city'];
$stmt = $conn->prepare("
SELECT * FROM users
WHERE full_name LIKE ?
AND age BETWEEN ? AND ?
AND city = ?
ORDER BY created_at DESC
");
// إضافة حروف البدل لبحث LIKE
$search_name = "%$search_name%";
// ربط المعلمات (سلسلة نصية، عدد صحيح، عدد صحيح، سلسلة نصية)
$stmt->bind_param("siis", $search_name, $min_age, $max_age, $city);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
echo $row['full_name'] . " - " . $row['city'] . "<br>";
}
$stmt->close();
?>
ربط النتائج (طريقة بديلة)
بدلاً من get_result()، يمكنك ربط أعمدة النتائج بمتغيرات:
<?php
require_once 'database.php';
$username = "johndoe";
$stmt = $conn->prepare("SELECT id, username, email FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
// ربط أعمدة النتائج بمتغيرات
$stmt->bind_result($id, $db_username, $email);
// جلب الصف
if ($stmt->fetch()) {
echo "المعرف: $id<br>";
echo "اسم المستخدم: $db_username<br>";
echo "البريد الإلكتروني: $email<br>";
} else {
echo "لم يتم العثور على المستخدم";
}
$stmt->close();
?>
ملاحظة:
get_result() أكثر مرونة وشائع الاستخدام، لكن bind_result() يستخدم ذاكرة أقل.
مثال تسجيل دخول آمن كامل
<?php
require_once 'database.php';
session_start();
function login($conn, $username, $password) {
// تحضير العبارة
$stmt = $conn->prepare("SELECT id, username, password, full_name FROM users WHERE username = ?");
// ربط المعلمة
$stmt->bind_param("s", $username);
// التنفيذ
$stmt->execute();
// الحصول على النتيجة
$result = $stmt->get_result();
if ($result->num_rows === 1) {
$user = $result->fetch_assoc();
// التحقق من كلمة المرور
if (password_verify($password, $user['password'])) {
// نجح - تعيين متغيرات الجلسة
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['full_name'] = $user['full_name'];
$stmt->close();
return true;
}
}
$stmt->close();
return false;
}
// معالجة تسجيل الدخول
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
if (login($conn, $username, $password)) {
header("Location: dashboard.php");
exit;
} else {
$error = "اسم المستخدم أو كلمة المرور غير صحيحة";
}
}
?>
فئة قاعدة بيانات قابلة لإعادة الاستخدام مع العبارات المحضرة
<?php
class Database {
private $conn;
public function __construct() {
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$this->conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
$this->conn->set_charset("utf8mb4");
}
// تنفيذ الاستعلام مع المعلمات
public function query($sql, $params = [], $types = "") {
$stmt = $this->conn->prepare($sql);
if (!empty($params)) {
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
return $stmt;
}
// استعلام SELECT - يُرجع جميع الصفوف
public function select($sql, $params = [], $types = "") {
$stmt = $this->query($sql, $params, $types);
$result = $stmt->get_result();
$rows = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return $rows;
}
// استعلام SELECT - يُرجع صف واحد
public function selectOne($sql, $params = [], $types = "") {
$stmt = $this->query($sql, $params, $types);
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$stmt->close();
return $row;
}
// استعلام INSERT - يُرجع آخر معرف إدراج
public function insert($sql, $params = [], $types = "") {
$stmt = $this->query($sql, $params, $types);
$insert_id = $stmt->insert_id;
$stmt->close();
return $insert_id;
}
// UPDATE/DELETE - يُرجع الصفوف المتأثرة
public function execute($sql, $params = [], $types = "") {
$stmt = $this->query($sql, $params, $types);
$affected = $stmt->affected_rows;
$stmt->close();
return $affected;
}
public function close() {
$this->conn->close();
}
}
// الاستخدام
$db = new Database();
// تحديد جميع المستخدمين
$users = $db->select("SELECT * FROM users WHERE city = ?", ["New York"], "s");
// تحديد مستخدم واحد
$user = $db->selectOne("SELECT * FROM users WHERE id = ?", [42], "i");
// إدراج مستخدم
$user_id = $db->insert(
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)",
["johndoe", "john@example.com", $hashed_password],
"sss"
);
// تحديث مستخدم
$affected = $db->execute(
"UPDATE users SET email = ? WHERE id = ?",
["newemail@example.com", 42],
"si"
);
// حذف مستخدم
$affected = $db->execute("DELETE FROM users WHERE id = ?", [42], "i");
?>
نصيحة: بناء جملة
...$params يفك حزمة المصفوفة لتمرير المعلمات بشكل فردي إلى bind_param().
أفضل ممارسات الأمان الإضافية
- التحقق من صحة المدخلات: تحقق من أنواع البيانات والتنسيقات قبل استخدامها
- تعقيم المخرجات: استخدم
htmlspecialchars()عند عرض بيانات المستخدم - تحديد الامتيازات: يجب أن يكون لمستخدمي قاعدة البيانات أقل الأذونات الضرورية
- استخدام HTTPS: تشفير نقل البيانات بين العميل والخادم
- تشفير كلمات المرور: استخدم دائمًا
password_hash()وpassword_verify() - تنفيذ تحديد المعدل: منع هجمات القوة الغاشمة
- تسجيل النشاط المشبوه: مراقبة وتسجيل محاولات تسجيل الدخول الفاشلة
تمرين: نظام تسجيل وتسجيل دخول آمن للمستخدمين
- أنشئ نموذج تسجيل يقبل: اسم المستخدم، البريد الإلكتروني، كلمة المرور، الاسم الكامل
- تحقق من صحة جميع المدخلات (الحقول المطلوبة، تنسيق البريد الإلكتروني، قوة كلمة المرور)
- استخدم العبارات المحضرة لإدراج المستخدم في قاعدة البيانات
- شفّر كلمة المرور بـ
password_hash() - تحقق من وجود اسم المستخدم/البريد الإلكتروني قبل الإدراج
- أنشئ نموذج تسجيل دخول يقبل اسم المستخدم وكلمة المرور
- استخدم العبارات المحضرة للاستعلام عن المستخدم
- تحقق من كلمة المرور بـ
password_verify() - عيّن متغيرات الجلسة عند نجاح تسجيل الدخول
- اختبر مع محاولات حقن SQL للتحقق من الأمان
اختبار حقن SQL
اختبر تطبيقك بهذه المدخلات الضارة للتأكد من أمانه:
// جرّب هذه في حقول اسم المستخدم/البريد الإلكتروني: ' OR '1'='1 ' OR '1'='1' -- ' OR '1'='1' /* admin'-- ' UNION SELECT NULL-- ' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055 1' ORDER BY 1--
ملاحظة: إذا كنت تستخدم العبارات المحضرة بشكل صحيح، يجب أن تفشل جميع هذه الهجمات بأمان دون التسبب في أخطاء أو وصول غير مصرح به.
الملخص
- حقن SQL هو ثغرة أمنية حرجة يمكن أن تعرض قاعدة البيانات بأكملها للخطر
- لا تدمج أبدًا مدخلات المستخدم مباشرة في استعلامات SQL
- استخدم دائمًا العبارات المحضرة للاستعلامات مع مدخلات المستخدم
- تفصل العبارات المحضرة كود SQL عن البيانات، مما يمنع الحقن
- استخدم محددات النوع المناسبة: i (عدد صحيح)، d (مزدوج)، s (سلسلة نصية)، b (ثنائي)
- استخدم
get_result()لجلب النتائج كمصفوفات ترابطية - اجمع العبارات المحضرة مع تشفير كلمة المرور للمصادقة الآمنة
- اختبر تطبيقك بمدخلات ضارة للتحقق من الأمان