حقن SQL والأمان
حقن SQL والأمان
🎯 تشبيه من الحياة
تخيل نقطة استلام الطرود:
- التدفق العادي: تقول "استلم طرد تشانغ سان"، والموظف يبحث بالاسم → استعلام عادي
- هجوم الحقن: تقول "استلم طرد تشانغ سان، وأعطني أيضاً طرد لي سي"، والموظف لا يميّز بعناية، فيعطيك كلا الطردين → حقن SQL
- تدابير الدفاع: يتطلب منك الموظف ملء نموذج استلام كامل، مع ملء كل حقل على حدة، ولا يقبل الطلبات المجمعة شفهياً → استعلام معلّم
📚 المفاهيم الأساسية
1. مبدأ حقن 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. تقنيات هجمات الحقن الشائعة
-- 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.
-- ❌ ربط النصوص (خطير)
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 + 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()
// 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 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 PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $username, 'password' => $password]);
4. منع الحقن عبر ORM
أطر ORM (الربط بين الكائنات والعلاقات) تستخدم عادةً الاستعلامات المعلمة تلقائياً، لكن يجب الحذر عند ربط SQL الخام:
# ✅ 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])
// ✅ 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. مبدأ أقل امتياز
يجب منح مستخدمي قاعدة البيانات فقط الامتيازات الأدنى اللازمة لإكمال مهامهم:
-- إنشاء مستخدم للقراءة فقط
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 |
| ❌ الهروب من النصوص | غير كافٍ، يجب ألا يكون الدفاع الوحيد |
💡 الصياغة الأساسية
-- 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 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 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 "اسم المستخدم أو كلمة المرور غير صالحة"
مثال: تأمين البحث مع منع الحقن وتحسين الأداء (الصعوبة ⭐⭐)
الطريقة الخطيرة:
// 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);
});
});
الطريقة الآمنة + المحسّنة:
// 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 - تسجيل مستخدم آمن
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: البناء الآمن لشروط التصفية الديناميكية
// 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 والدفاع عنها:
- مبدأ الحقن: إدخال المستخدم يُنفذ ككود SQL، ناتج عن ربط النصوص
- تقنيات الهجوم: تجاوز تسجيل الدخول، استعلامات UNION، الاستعلامات المتراكبة، الحقن الأعمى، إلخ.
- الدفاع الأساسي: الاستعلامات المعلمة (Prepared Statements)، فصل بنية SQL عن البيانات
- أطر ORM: المعلمات التلقائية، لكن يبقى اليقظة عند ربط SQL الخام
- أقل امتياز: منح فقط الأذونات اللازمة لمستخدمي قاعدة البيانات؛ لا تستخدم root لاتصالات التطبيق أبداً
- الدفاع المعمّق: المعلمات + التحقق من الإدخال + التحكم في الوصول + إخفاء الأخطاء + WAF
📝 تمارين
- ابحث عن ثغرة حقن SQL في الكود التالي وأصلحها:SQL
query = "SELECT * FROM products WHERE category = '" + category + "' ORDER BY " + sortField - اكتب كود استعلام معلّم آمن لميزة بحث المستخدم (دعم البحث الضبابي، التصفح، والفرز).
- أنشئ ثلاثة مستخدمين لقاعدة بيانات لمشروعك: مستخدم للقراءة فقط، مستخدم للتطبيق، ومستخدم مسؤول، مع منح الأذونات المناسبة لكل منهم.
الدرس التالي ←27-optimization.md



