SQL: حقن SQL والأمان
آخر تحديث: 2026-10-09
1. 🎯 تشبيه من الحياة
تخيل نقطة استلام الطرود:
- التدفق العادي: تقول "استلم طرد تشانغ سان"، والموظف يبحث بالاسم ← استعلام عادي
- هجوم الحقن: تقول "استلم طرد تشانغ سان، وأعطني أيضاً طرد لي سي"، والموظف لا يميّز بعناية، فيعطيك كلا الطردين ← حقن SQL
- تدابير الدفاع: يتطلب منك الموظف ملء نموذج استلام كامل، مع ملء كل حقل على حدة، ولا يقبل الطلبات المجمعة شفهياً ← استعلام معلّم
2. 📚 المفاهيم الأساسية
(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 |
| ❌ الهروب من النصوص | غير كافٍ، يجب ألا يكون الدفاع الوحيد |
3. 💡 الصياغة الأساسية
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: 'خدمة البحث غير متاحة مؤقتاً' });
}
});
4. 🏢 السيناريو 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
5. 🏢 السيناريو 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 والدفاع عنها:
- مبدأ الحقن: إدخال المستخدم يُنفذ ككود SQL، ناتج عن ربط النصوص
- تقنيات الهجوم: تجاوز تسجيل الدخول، استعلامات UNION، الاستعلامات المتراكبة، الحقن الأعمى، إلخ.
- الدفاع الأساسي: الاستعلامات المعلمة (Prepared Statements)، فصل بنية SQL عن البيانات
- أطر ORM: المعلمات التلقائية، لكن يبقى اليقظة عند ربط SQL الخام
- أقل امتياز: منح فقط الأذونات اللازمة لمستخدمي قاعدة البيانات؛ لا تستخدم root لاتصالات التطبيق أبداً
- الدفاع المعمّق: المعلمات + التحقق من الإدخال + التحكم في الوصول + إخفاء الأخطاء + WAF
📝 تمارين
- ابحث عن ثغرة حقن SQL في الكود التالي وأصلحها:SQL
query = "SELECT * FROM products WHERE category = '" + category + "' ORDER BY " + sortField - اكتب كود استعلام معلّم آمن لميزة بحث المستخدم (دعم البحث الضبابي، التصفح، والفرز).
- أنشئ ثلاثة مستخدمين لقاعدة بيانات لمشروعك: مستخدم للقراءة فقط، مستخدم للتطبيق، ومستخدم مسؤول، مع منح الأذونات المناسبة لكل منهم.
الدرس التالي ←27-optimization.md