حقن SQL والأمان

حقن SQL والأمان

🎯 تشبيه من الحياة

تخيل نقطة استلام الطرود:

📚 المفاهيم الأساسية

1. مبدأ حقن SQL

جوهر حقن SQL هو: إدخال المستخدم يُنفذ ككود SQL.

عندما يبني البرنامج SQL باستخدام ربط النصوص، يمكن للمهاجمين إدراج أجزاء SQL خبيثة.

SQL
-- كود الخلفية (كود وهمي)
sql = "SELECT * FROM users WHERE username = '" + input_user + "' AND password = '" + input_pass + "'"

-- إدخال عادي
-- input_user = "admin"
-- input_pass = "123456"
-- SQL المُنشأ:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'

-- إدخال خبيث
-- input_user = "admin' --"
-- input_pass = "anything"
-- SQL المُنشأ:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
-- '--' تعليق SQL، باقي الشرط يُعلّق، يتم تجاوز التحقق من كلمة المرور!

2. تقنيات هجمات الحقن الشائعة

SQL
-- 1. تجاوز التحقق من تسجيل الدخول
-- الإدخال: admin' OR '1'='1' --
SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = ''

-- 2. استعلام UNION لسرقة البيانات
-- الإدخال: ' UNION SELECT username, password FROM users --
SELECT * FROM products WHERE name = '' UNION SELECT username, password FROM users --'

-- 3. استعلامات متراكبة لتنفيذ عدة جمل
-- الإدخال: '; DROP TABLE users; --
SELECT * FROM products WHERE name = ''; DROP TABLE users; --'

-- 4. حقن أعمى منطقي (استنتاج البيانات من خلال اختلافات الاستجابة)
-- الإدخال: ' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --
SELECT * FROM products WHERE name = '' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --'

3. الاستعلام المعلّم (Prepared Statement)

الفكرة الأساسية: يتم فصل بنية SQL عن البيانات؛ محرك قاعدة البيانات لن ينفذ قيم المعلمات ككود SQL.

SQL
-- ❌ ربط النصوص (خطير)
SET @username = "admin' --";
SET @sql = CONCAT('SELECT * FROM users WHERE username = ''', @username, '''');
PREPARE stmt FROM @sql;
EXECUTE stmt;

-- ✅ استعلام معلّم (آمن)
SET @username = "admin' --";
PREPARE stmt FROM 'SELECT * FROM users WHERE username = ?';
EXECUTE stmt USING @username;
--قاعدة البيانات تتعامل مع @username بأكمله كقيمة نصية واحدة، ولا تنفذ أي أجزاء SQL بداخلها

الاستعلامات المعلمة في لغات مختلفة:

PYTHON
# Python + MySQL Connector
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

# Python + SQLAlchemy ORM
user = session.query(User).filter(User.username == username, User.password == password).first()
JAVASCRIPT
// Node.js + mysql2
const [rows] = await pool.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);

// Node.js + Prisma ORM
const user = await prisma.user.findFirst({ where: { username, password } });
JAVA
// Java JDBC
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
PHP
// PHP PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $username, 'password' => $password]);

4. منع الحقن عبر ORM

أطر ORM (الربط بين الكائنات والعلاقات) تستخدم عادةً الاستعلامات المعلمة تلقائياً، لكن يجب الحذر عند ربط SQL الخام:

PYTHON
# ✅ ORM المعلّم تلقائياً (آمن)
User.objects.filter(username=username, password=password)

# ❌ ربط SQL الخام في ORM (خطير)
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")

# ✅ الطريقة الآمنة لاستخدام SQL الخام في ORM
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
JAVASCRIPT
// ✅ Sequelize ORM المعلّم تلقائياً
await User.findOne({ where: { username, password } });

// ❌ Sequelize الاستعلام المربوط (خطير)
await sequelize.query(`SELECT * FROM users WHERE username = '${username}'`);

// ✅ Sequelize الطريقة الآمنة
await sequelize.query("SELECT * FROM users WHERE username = ?", { replacements: [username] });

5. مبدأ أقل امتياز

يجب منح مستخدمي قاعدة البيانات فقط الامتيازات الأدنى اللازمة لإكمال مهامهم:

SQL
-- إنشاء مستخدم للقراءة فقط
CREATE USER 'app_readonly'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'app_readonly'@'%';

-- إنشاء مستخدم التطبيق (يسمح فقط بـ CRUD، بدون DROP/ALTER)
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';

-- منع انتشار الامتيازات
REVOKE GRANT OPTION ON mydb.* FROM 'app_user'@'%';

-- مستخدم المسؤول (للداخلي فقط)
CREATE USER 'db_admin'@'localhost' IDENTIFIED BY 'very_strong_password';
GRANT ALL PRIVILEGES ON mydb.* TO 'db_admin'@'localhost';

6. قائمة التحقق من الدفاع

إجراء الدفاع الوصف
✅ استعلامات معلمة استخدم مقاييس المعلمات لجميع استعلامات SQL
✅ أطر ORM فضّل ORM لتقليل SQL المكتوب يدوياً
✅ التحقق من الإدخال تحقق من الطول والنمط والصيغة (القائمة البيضاء أفضل من القائمة السوداء)
✅ أقل امتياز امنح فقط الأذونات اللازمة لمستخدمي قاعدة البيانات
✅ رسائل الخطأ لا تكشف تفاصيل خطأ قاعدة البيانات للمستخدمين
✅ حماية WAF قم بنشر جدار حماية تطبيقات الويب
✅ المراجعات الدورية راجع الكود للبحث عن ربط SQL
❌ الهروب من النصوص غير كافٍ، يجب ألا يكون الدفاع الوحيد

💡 الصياغة الأساسية

SQL
-- MySQL Prepared Statement
PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?';
SET @id = 1;
EXECUTE stmt USING @id;
DEALLOCATE PREPARE stmt;

-- الطريقة الآمنة لاستعلامات LIKE في MySQL
PREPARE stmt FROM 'SELECT * FROM users WHERE name LIKE ?';
SET @pattern = CONCAT('%', 'keyword', '%');
EXECUTE stmt USING @pattern;

-- الطريقة الآمنة لعبارات IN (تتطلب بناء ديناميكي)
-- الطريقة: استخدم FIND_IN_SET أو جدول مؤقت
PREPARE stmt FROM 'SELECT * FROM users WHERE FIND_IN_SET(id, ?)';
SET @ids = '1,2,3';
EXECUTE stmt USING @ids;
💡 نصيحة: لا تثق أبداً في إدخال المستخدم. حتى القوائم المنسدلة والحقول المخفية يمكن التلاعب بها؛ يجب على الخادم التحقق من جميع المدخلات وجعلها معلمات.

مثال: تأمين نقطة تسجيل الدخول ضد الحقن (الصعوبة ⭐)

قبل (خطير):

PYTHON
# Python Flask - الطريقة الخطيرة
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    # ❌ ربط النصوص، معرض لحقن SQL
    sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    user = db.execute(sql).fetchone()
    if user:
        return "تسجيل الدخول ناجح"
    return "اسم المستخدم أو كلمة المرور غير صالحة"
▶ جرّب الكود

بعد (آمن):

PYTHON
# Python Flask - الطريقة الآمنة
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # التحقق من الإدخال
    if not username or len(username) > 50:
        return "صيغة اسم المستخدم غير صالحة", 400
    if not password or len(password) > 100:
        return "صيغة كلمة المرور غير صالحة", 400

    # ✅ استعلام معلّم
    user = db.execute(
        "SELECT * FROM users WHERE username = ? AND password = ?",
        (username, password)
    ).fetchone()

    if user:
        return "تسجيل الدخول ناجح"
    return "اسم المستخدم أو كلمة المرور غير صالحة"

مثال: تأمين البحث مع منع الحقن وتحسين الأداء (الصعوبة ⭐⭐)

الطريقة الخطيرة:

JAVASCRIPT
// Node.js Express - الطريقة الخطيرة
app.get('/search', (req, res) => {
    const keyword = req.query.q;
    // ❌ ربط SQL، معرض للحقن
    const sql = `SELECT * FROM articles WHERE title LIKE '%${keyword}%' OR content LIKE '%${keyword}%'`;
    db.query(sql, (err, results) => {
        res.json(results);
    });
});
▶ جرّب الكود

الطريقة الآمنة + المحسّنة:

JAVASCRIPT
// Node.js Express - الطريقة الآمنة + المحسّنة
app.get('/search', async (req, res) => {
    try {
        const keyword = req.query.q?.trim();

        // التحقق من الإدخال
        if (!keyword || keyword.length > 100) {
            return res.status(400).json({ error: 'يجب أن تكون كلمة البحث من 1 إلى 100 حرف' });
        }

        // ✅ استعلام معلّم + تحسين الفهرسة النصية الكاملة
        const sql = `
            SELECT id, title, LEFT(content, 200) AS snippet, created_at
            FROM articles
            WHERE status = 1
              AND MATCH(title, content) AGAINST(? IN BOOLEAN MODE)
            ORDER BY MATCH(title, content) AGAINST(? IN BOOLEAN MODE) DESC
            LIMIT 20
        `;
        const [rows] = await pool.execute(sql, [keyword, keyword]);
        res.json(rows);
    } catch (err) {
        // لا تكشف تفاصيل خطأ قاعدة البيانات
        console.error('خطأ في البحث:', err);
        res.status(500).json({ error: 'خدمة البحث غير متاحة مؤقتاً' });
    }
});

🏢 السيناريو 1: أمان نقطة تسجيل المستخدم

PYTHON
# Python - تسجيل مستخدم آمن
import re
import hashlib

@app.route('/register', methods=['POST'])
def register():
    username = request.form.get('username', '').strip()
    email = request.form.get('email', '').strip()
    password = request.form.get('password', '')

    # 1. التحقق من الإدخال (قائمة بيضاء)
    if not re.match(r'^[a-zA-Z0-9_]{3,50}$', username):
        return "اسم المستخدم يمكن أن يحتوي فقط على أحرف وأرقام وشرطات سفلية، 3-50 حرف", 400
    if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email) or len(email) > 100:
        return "صيغة البريد الإلكتروني غير صالحة", 400
    if len(password) < 8 or len(password) > 100:
        return "يجب أن تكون كلمة المرور من 8 إلى 100 حرف", 400

    # 2. استعلام معلّم للتحقق من الوجود
    existing = db.execute(
        "SELECT id FROM users WHERE username = ? OR email = ?",
        (username, email)
    ).fetchone()
    if existing:
        return "اسم المستخدم أو البريد الإلكتروني مسجل بالفعل", 409

    # 3. تخزين تجزئة كلمة المرور
    password_hash = hashlib.sha256(password.encode()).hexdigest()

    # 4. إدخال معلّم
    db.execute(
        "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
        (username, email, password_hash)
    )
    db.commit()

    return "تم التسجيل بنجاح", 201

🏢 السيناريو 2: البناء الآمن لشروط التصفية الديناميكية

JAVASCRIPT
// Node.js - بناء استعلام ديناميكي آمن
function buildProductQuery(filters) {
    let sql = 'SELECT * FROM products WHERE 1=1';
    const params = [];

    if (filters.categoryId) {
        sql += ' AND category_id = ?';
        params.push(filters.categoryId);
    }

    if (filters.minPrice !== undefined) {
        sql += ' AND price >= ?';
        params.push(filters.minPrice);
    }

    if (filters.maxPrice !== undefined) {
        sql += ' AND price <= ?';
        params.push(filters.maxPrice);
    }

    if (filters.keyword) {
        sql += ' AND name LIKE ?';
        params.push(`%${filters.keyword}%`);
    }

    if (filters.status !== undefined) {
        sql += ' AND status = ?';
        params.push(filters.status);
    }

    // قائمة بيضاء لحقول الفرز
    const allowedSort = ['price', 'created_at', 'sales_count'];
    const sortBy = allowedSort.includes(filters.sortBy) ? filters.sortBy : 'created_at';
    const sortOrder = filters.sortOrder === 'ASC' ? 'ASC' : 'DESC';
    sql += ` ORDER BY ${sortBy} ${sortOrder}`;

    // التحقق من معلمات التصفح
    const page = Math.max(1, parseInt(filters.page) || 1);
    const pageSize = Math.min(100, Math.max(1, parseInt(filters.pageSize) || 20));
    sql += ' LIMIT ? OFFSET ?';
    params.push(pageSize, (page - 1) * pageSize);

    return { sql, params };
}

// الاستخدام
app.get('/products', async (req, res) => {
    const { sql, params } = buildProductQuery(req.query);
    const [rows] = await pool.execute(sql, params);
    res.json(rows);
});
💡 مفتاح: حقول الفرز لا يمكنها استخدام مقاييس المعلمات مباشرة (لأنها أسماء أعمدة وليست قيماً) ويجب التحقق منها بقائمة بيضاء.

❓ أسئلة شائعة

س: هل تؤثر الاستعلامات المعلمة على الأداء؟ ج: لا. الاستعلامات المعلمة يمكنها أيضاً الاستفادة من ذاكرة التخزين المؤقت المُجمّعة مسبقاً في قاعدة البيانات، مما يجعل التنفيذ المتكرر لنفس قالب الاستعلام أسرع.

س: هل أطر ORM آمنة دائماً؟ ج: ليس بالضرورة. المعلمات التلقائية في ORM آمنة، لكن إذا استخدمت raw() أو ربط النصوص لبناء الاستعلامات، لا يزال هناك خطر الحقن. الأطر مجرد أدوات؛ الوعي الأمني هو ما يهم.

س: هل يمكن للإجراءات المخزنة منع الحقن؟ ج: الإجراءات المخزنة نفسها لا تمنع الحقن. إذا استخدم إجراء مخزن ربط SQL داخلياً، فهو معرض بالتساوي. المفتاح هو استخدام المعلمات.

س: هل يكفي التحقق من الواجهة الأمامية وحده؟ ج: مطلقاً. يمكن تجاوز التحقق من الواجهة الأمامية (الاستدعاء المباشر لـ APIs، تعديل الطلبات). يجب تنفيذ جميع عمليات التحقق الأمني على جانب الخادم.

📖 ملخص

غطي هذا الدرس بشكل معمّق هجمات حقن SQL والدفاع عنها:

📝 تمارين

  1. ابحث عن ثغرة حقن SQL في الكود التالي وأصلحها:
    SQL
    query = "SELECT * FROM products WHERE category = '" + category + "' ORDER BY " + sortField
    
  2. اكتب كود استعلام معلّم آمن لميزة بحث المستخدم (دعم البحث الضبابي، التصفح، والفرز).
  3. أنشئ ثلاثة مستخدمين لقاعدة بيانات لمشروعك: مستخدم للقراءة فقط، مستخدم للتطبيق، ومستخدم مسؤول، مع منح الأذونات المناسبة لكل منهم.

الدرس التالي ←27-optimization.md

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%