أساسيات PHP

أفضل ممارسات الأمان

15 دقيقة الدرس 44 من 45

أفضل ممارسات الأمان في PHP

الأمان هو أمر بالغ الأهمية في تطوير الويب. يمكن لثغرة أمنية واحدة أن تعرض تطبيقك بالكامل وبيانات المستخدمين للخطر. في هذا الدرس، سنغطي الممارسات الأمنية الأساسية التي يجب على كل مطور PHP اتباعها.

1. التحقق من صحة المدخلات وتنقيتها

لا تثق أبدًا في إدخالات المستخدم. تحقق دائمًا من صحة البيانات وقم بتنقيتها من النماذج وعناوين URL وملفات تعريف الارتباط.

القاعدة الذهبية: تحقق من المدخلات، واهرب من المخرجات، ولا تثق أبدًا في البيانات من مصادر خارجية.

التحقق من صحة المدخلات

<?php
// التحقق من البريد الإلكتروني
$email = $_POST['email'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    die('عنوان بريد إلكتروني غير صالح');
}

// التحقق من رقم صحيح
$id = $_GET['id'] ?? 0;
if (!filter_var($id, FILTER_VALIDATE_INT) || $id < 1) {
    die('معرف غير صالح');
}

// التحقق من URL
$website = $_POST['website'] ?? '';
if (!filter_var($website, FILTER_VALIDATE_URL)) {
    die('رابط غير صالح');
}

// التحقق بقائمة بيضاء للقيم المحددة
$action = $_GET['action'] ?? '';
$allowed_actions = ['view', 'edit', 'delete'];
if (!in_array($action, $allowed_actions)) {
    die('إجراء غير صالح');
}

// التحقق من الطول
$username = $_POST['username'] ?? '';
if (strlen($username) < 3 || strlen($username) > 20) {
    die('يجب أن يكون اسم المستخدم من 3-20 حرفًا');
}

// التحقق بالتعبيرات النمطية للأحرف والأرقام
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
    die('يمكن أن يحتوي اسم المستخدم على أحرف وأرقام وشرطات سفلية فقط');
}
?>

تنقية المدخلات

<?php
// تنقية البريد الإلكتروني
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);

// تنقية النص (إزالة الوسوم)
$name = filter_var($_POST['name'], FILTER_SANITIZE_STRING);

// إزالة وسوم HTML
$comment = strip_tags($_POST['comment']);

// إزالة وسوم محددة مع الاحتفاظ بأخرى
$content = strip_tags($_POST['content'], '<p><br><strong><em>');

// إزالة المسافات البيضاء
$input = trim($_POST['input']);

// إزالة الأحرف الخاصة
$clean = preg_replace('/[^a-zA-Z0-9]/', '', $input);
?>

2. منع حقن SQL

حقن SQL هو واحد من أخطر الثغرات الأمنية. استخدم دائمًا العبارات المُعدة مسبقًا.

لا تفعل هذا أبدًا: لا تدمج إدخال المستخدم مباشرة في استعلامات SQL!
<?php
// ❌ غير آمن - لا تفعل هذا أبدًا!
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
// يمكن للمهاجم إدخال: ' OR '1'='1

// ✅ آمن - استخدم العبارات المُعدة مسبقًا
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);

// ✅ آمن - معاملات مسماة
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND email = :email");
$stmt->execute([
    'username' => $username,
    'email' => $email
]);

// ✅ آمن - عبارات MySQLi المُعدة مسبقًا
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
?>

3. منع البرمجة النصية عبر المواقع (XSS)

هجمات XSS تحقن JavaScript ضار في صفحاتك. قم دائمًا بالهروب من المخرجات.

<?php
// الحصول على إدخال المستخدم
$comment = $_POST['comment'];

// التخزين في قاعدة البيانات (بعبارات مُعدة مسبقًا)
$stmt = $pdo->prepare("INSERT INTO comments (text) VALUES (?)");
$stmt->execute([$comment]);

// عند العرض، اهرب دائمًا
$comment = $row['text'];
?>

<!-- عرض المخرجات الآمنة -->
<div class="comment">
    <?php echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); ?>
</div>

<?php
// دالة مساعدة للهروب
function e($string) {
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
?>

<div><?php echo e($user_input); ?></div>
نصيحة احترافية: محركات القوالب الحديثة مثل Twig و Blade تهرب من المخرجات تلقائيًا افتراضيًا.

4. منع تزوير الطلبات عبر المواقع (CSRF)

هجمات CSRF تخدع المستخدمين لتنفيذ إجراءات غير مرغوب فيها. استخدم رموز CSRF لجميع العمليات التي تغير الحالة.

<?php
// توليد رمز CSRF
session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// التضمين في النماذج
?>
<form method="POST" action="delete.php">
    <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
    <button type="submit">حذف الحساب</button>
</form>

<?php
// التحقق من رمز CSRF عند إرسال النموذج
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';

    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        die('فشل التحقق من رمز CSRF');
    }

    // معالجة النموذج...
}

// دالة حماية CSRF
function verify_csrf_token($token) {
    return isset($_SESSION['csrf_token']) &&
           hash_equals($_SESSION['csrf_token'], $token);
}
?>

5. أمان كلمات المرور

لا تخزن كلمات المرور بنص عادي أبدًا. استخدم دوال تجزئة كلمات المرور المدمجة في PHP.

<?php
// تجزئة كلمة المرور عند التسجيل
$password = $_POST['password'];
$hashed = password_hash($password, PASSWORD_DEFAULT);

// تخزين $hashed في قاعدة البيانات
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$username, $hashed]);

// التحقق من كلمة المرور عند تسجيل الدخول
$stmt = $pdo->prepare("SELECT password FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();

if ($user && password_verify($_POST['password'], $user['password'])) {
    // كلمة المرور صحيحة - تسجيل دخول المستخدم
    $_SESSION['user_id'] = $user['id'];
    header('Location: dashboard.php');
} else {
    // بيانات اعتماد غير صالحة
    echo 'اسم مستخدم أو كلمة مرور غير صالحة';
}

// التحقق مما إذا كانت كلمة المرور تحتاج إعادة تجزئة (تحديث الخوارزمية)
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
    $new_hash = password_hash($password, PASSWORD_DEFAULT);
    // تحديث قاعدة البيانات بالتجزئة الجديدة
}
?>
متطلبات كلمة المرور: فرض الحد الأدنى للطول (8+ أحرف)، والتعقيد، وفكر في استخدام أدوات قياس قوة كلمة المرور.

6. أمان الجلسات

معالجة الجلسات الآمنة تمنع اختطاف الجلسات وهجمات التثبيت.

<?php
// تكوين جلسة آمنة
ini_set('session.cookie_httponly', 1);  // منع وصول JavaScript
ini_set('session.cookie_secure', 1);    // HTTPS فقط
ini_set('session.use_strict_mode', 1);  // رفض معرفات الجلسة غير المهيأة
ini_set('session.cookie_samesite', 'Strict');  // حماية CSRF

session_start();

// تجديد معرف الجلسة عند تسجيل الدخول
function login_user($user_id) {
    session_regenerate_id(true);  // منع تثبيت الجلسة
    $_SESSION['user_id'] = $user_id;
    $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}

// التحقق من صحة الجلسة
function validate_session() {
    if (!isset($_SESSION['user_id'])) {
        return false;
    }

    // التحقق من عنوان IP (اختياري - قد يسبب مشاكل مع مستخدمي الهاتف)
    if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        return false;
    }

    // التحقق من وكيل المستخدم
    if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        session_destroy();
        return false;
    }

    return true;
}

// تسجيل الخروج
function logout() {
    $_SESSION = [];
    session_destroy();
    setcookie(session_name(), '', time() - 3600, '/');
}
?>

7. أمان رفع الملفات

رفع الملفات هو ناقل هجوم شائع. تحقق دائمًا من صحة الملفات المرفوعة ونقِّها.

<?php
// معالجة رفع ملف آمن
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
    $file = $_FILES['avatar'];

    // التحقق من أخطاء الرفع
    if ($file['error'] !== UPLOAD_ERR_OK) {
        die('خطأ في الرفع');
    }

    // التحقق من حجم الملف (5 ميجابايت كحد أقصى)
    $max_size = 5 * 1024 * 1024;
    if ($file['size'] > $max_size) {
        die('الملف كبير جدًا');
    }

    // التحقق من نوع MIME
    $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime_type = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);

    if (!in_array($mime_type, $allowed_types)) {
        die('نوع ملف غير صالح');
    }

    // التحقق من امتداد الملف
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

    if (!in_array($extension, $allowed_extensions)) {
        die('امتداد ملف غير صالح');
    }

    // توليد اسم ملف فريد
    $new_filename = bin2hex(random_bytes(16)) . '.' . $extension;
    $upload_dir = 'uploads/';
    $destination = $upload_dir . $new_filename;

    // نقل الملف خارج جذر الويب إن أمكن
    if (move_uploaded_file($file['tmp_name'], $destination)) {
        // تخزين اسم الملف في قاعدة البيانات
        echo 'تم رفع الملف بنجاح';
    }
}
?>
حرج: لا تثق أبدًا في اسم الملف الأصلي أو امتداد الملف. تحقق دائمًا من محتوى الملف الفعلي.

8. منع اجتياز الدليل

امنع المهاجمين من الوصول إلى الملفات خارج الدليل المقصود.

<?php
// ❌ غير آمن
$file = $_GET['file'];
include("pages/" . $file);
// يمكن للمهاجم استخدام: ../../etc/passwd

// ✅ آمن - نهج القائمة البيضاء
$allowed_files = ['home', 'about', 'contact'];
$file = $_GET['page'] ?? 'home';

if (in_array($file, $allowed_files)) {
    include("pages/{$file}.php");
} else {
    include("pages/404.php");
}

// ✅ آمن - تنقية المسار
function safe_include($filename) {
    // إزالة تسلسلات اجتياز الدليل
    $filename = str_replace(['../', '..\\'], '', $filename);

    // بناء المسار الكامل
    $base_dir = __DIR__ . '/pages/';
    $full_path = realpath($base_dir . $filename . '.php');

    // التحقق من أن الملف ضمن الدليل الأساسي
    if ($full_path && strpos($full_path, $base_dir) === 0 && file_exists($full_path)) {
        include($full_path);
    } else {
        include($base_dir . '404.php');
    }
}
?>

9. معالجة الأخطاء والتسجيل

لا تكشف عن معلومات حساسة في رسائل الخطأ.

<?php
// إعدادات الإنتاج في php.ini أو .htaccess
ini_set('display_errors', 0);  // لا تظهر الأخطاء للمستخدمين
ini_set('log_errors', 1);       // سجل الأخطاء في ملف
ini_set('error_log', '/path/to/php-errors.log');

// معالج أخطاء مخصص
set_error_handler(function($errno, $errstr, $errfile, $errline) {
    // تسجيل الخطأ
    error_log("Error [$errno]: $errstr in $errfile on line $errline");

    // إظهار رسالة عامة للمستخدم
    if (ini_get('display_errors')) {
        echo "<b>خطأ:</b> $errstr";
    } else {
        echo "حدث خطأ. يرجى المحاولة مرة أخرى لاحقًا.";
    }
});

// معالج استثناءات مخصص
set_exception_handler(function($exception) {
    error_log($exception->getMessage());

    if (!ini_get('display_errors')) {
        die('حدث خطأ غير متوقع.');
    }
});

// Try-catch لأخطاء قاعدة البيانات
try {
    $pdo = new PDO($dsn, $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("Database error: " . $e->getMessage());
    die('فشل الاتصال بقاعدة البيانات');  // لا تكشف التفاصيل
}
?>

10. رؤوس الأمان

عيّن رؤوس أمان HTTP للحماية من الهجمات الشائعة.

<?php
// تعيين رؤوس الأمان
header("X-Frame-Options: DENY");  // منع clickjacking
header("X-Content-Type-Options: nosniff");  // منع استنشاق MIME
header("X-XSS-Protection: 1; mode=block");  // حماية XSS
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'");  // CSP

// لمواقع HTTPS
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
    header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
}
?>

تمرين قائمة التحقق الأمنية

راجع تطبيق PHP وتحقق من:

  • تم التحقق من صحة جميع إدخالات المستخدم وتنقيتها
  • جميع استعلامات قاعدة البيانات تستخدم عبارات مُعدة مسبقًا
  • جميع المخرجات تم الهروب منها بشكل صحيح
  • رموز CSRF تحمي جميع النماذج
  • كلمات المرور مجزأة بـ password_hash()
  • الجلسات مكونة بشكل آمن
  • رفع الملفات محقق منه تمامًا
  • رسائل الخطأ لا تكشف عن معلومات حساسة
  • رؤوس الأمان معينة
  • HTTPS مفروض

موارد أمان إضافية

  • OWASP Top 10: ادرس مخاطر أمان تطبيقات الويب الأكثر أهمية
  • PHP Security Cheat Sheet: مرجع سريع للبرمجة الآمنة
  • اختبار الأمان: استخدم أدوات مثل RIPS و PHPStan للتحليل الثابت
  • فحص التبعيات: استخدم Composer audit للتحقق من الحزم الضعيفة
  • اختبار الاختراق: فكر في عمليات تدقيق أمنية احترافية لتطبيقات الإنتاج
تذكر: الأمان ليس مهمة لمرة واحدة. ابق محدثًا بتصحيحات الأمان، وراجع الكود بانتظام، واتبع أفضل ممارسات الأمان طوال التطوير.