حقن SQL المنع
لا يتم إدراج القيم المقدمة من المستخدم داخل نص SQL مطلقًا — بل تُربط عبر placeholders للمعاملات (حسب اللهجة، مثل $1 أو ?)
فهم حقن SQL
يبقى حقن SQL من أخطر ثغرات تطبيقات الويب
يحدث حقن SQL عندما تُضمَّن بيانات غير موثوقة داخل نص SQL بدون ربط معاملات آمن. يمكن للمهاجمين تغيير معنى الاستعلام للوصول إلى بيانات غير مصرح بها أو تعديل سجلات أو تنفيذ عمليات غير مقصودة.
أنماط هجوم شائعة
تجاوز المصادقة
admin' OR '1'='1 محاولة تجاوز تسجيل الدخول بجعل شرط WHERE صحيحًا دائمًا
استخراج البيانات
' UNION SELECT password FROM users-- استخدام UNION لمحاولة استخراج بيانات من جداول أخرى
أوامر تدميرية
'; DROP TABLE users; -- محاولة إلحاق تعليمات إضافية لحذف البيانات
استغلال خطأ ترتيب المعاملات
Exploit: If placeholders and params are mismatched, authorization checks may use the wrong values مثال: WHERE userId=$1 AND isAdmin=$2، لكن params=[true, 123] بدلًا من [123, true] قد يمنح صلاحيات admin بشكل خاطئ
استراتيجية حماية متعددة الطبقات
يمر كل استعلام عبر طبقات متعددة من التحقق الأمني
الطبقة 1: ربط معاملات تلقائي
تُحوَّل كل قيم المستخدم إلى معاملات مربوطة. لا تدخل القيم نص SQL كحروفية.
- تمثيل قيم المستخدم عبر placeholders خاصة باللهجة (مثل $1 أو ?)
- مُدخلات المصفوفات تُعلَّم (توسيع placeholders أو معاملات array حسب اللهجة/الاستراتيجية)
- معالجة NULL والقيم الاختيارية بدون وصل إدخال المستخدم بنص SQL
- قيم التاريخ/الوقت تُعلَّم
- قيم JSON تُعلَّم
الطبقة 2: التحقق من أسماء الحقول
يتم التحقق من كل اسم حقل مقابل بيانات مخطط Prisma قبل توليد الاستعلام.
- السماح فقط بالحقول المعرفة في المخطط
- التحقق من مرشحات العلاقات عبر بيانات المخطط
- لا تُقبل أسماء حقول عشوائية
- رفض الحقول غير المدعومة/المحسوبة عند الحاجة
- رفض حمولات prototype pollution (مثل __proto__ وconstructor)
الطبقة 3: تنقية المعرّفات
التحقق من أسماء الجداول والأعمدة والـaliases واقتباسها/هروبها بأمان حسب الحاجة.
- رفض أحرف التحكم
- اقتباس الكلمات المحجوزة عند الحاجة
- دعم تأهيل المخطط عند الإعداد
- هروب علامات الاقتباس المزدوجة (أو ما يعادلها) للمعرّفات
- فرض الحد الأقصى لطول المعرّف حسب اللهجة
الطبقة 4: التحقق من العوامل (Operators)
السماح فقط بعوامل معروفة وآمنة لكل نوع حقل.
- عوامل النص: contains, startsWith, endsWith
- عوامل الأرقام: lt, lte, gt, gte
- عوامل المصفوفات: in, notIn مع تحقق النوع
- العوامل المنطقية: AND, OR, NOT تُتحقق بنيويًا
- رفض العوامل غير المعروفة فورًا
الطبقة 5: ضمان ترتيب المعاملات
ترتيب صارم يضمن أن كل placeholder يطابق معاملًا واحدًا بالضبط حسب ترتيب الإدراج لمنع عدم التطابق.
- عداد تسلسلي يمنع إعادة الترتيب
- بناء SQL ومصفوفة المعاملات بخطوة متزامنة
- لا توجد مخازن أو عمليات إعادة ترتيب وسيطة
- تحقق الاختبارات: SQL المُولّد والمعاملات دائمًا متطابقان
- مطابقة 1–1: placeholder رقم N → params[N-1] (أو ما يعادلها حسب اللهجة)
ضمانات أمنية شكلية
ملخصات براهين لمقاومة الحقن وفق افتراضات التصميم (ربط معاملات، لا تجاوز raw SQL، استخدام صحيح للسائق)
المبرهنة 1: عزل القيم
For all user-provided values V, there exists no execution path where V appears as SQL text in the generated query string.
By construction, all code paths that handle user values call addParameter(value) which: 1. Stores the value in a separate params array 2. Returns a placeholder token for the SQL text 3. Only the placeholder token is appended into the SQL string 4. The database driver receives SQL text and parameter values separately ∴ User values are not parsed as SQL syntax
const addParameter = (params: any[], value: any) => {
params.push(value)
const index = params.length
return `$${index}`
} المبرهنة 2: إغلاق أسماء الحقول
The set of field names F in any generated query is a subset of schema-defined fields S, i.e., F ⊆ S.
For every field reference: 1. Field name extracted from query object 2. Lookup performed against schema metadata 3. If the field does not exist in the schema, an error is thrown 4. Only validated fields reach SQL generation ∴ Arbitrary field names cannot appear in SQL
const validateField = (field: string, model: Model) => {
if (!model.fields.has(field)) {
throw new Error(`Field ${field} does not exist`)
}
return model.fields.get(field)
} المبرهنة 3: أمان العوامل
For any operator O applied to field F, O is a member of the allowed operators for F's type T.
Operator validation algorithm: 1. Extract field type T from schema 2. Define allowed_ops(T) = { valid operators for type T } 3. For operator O in query: - If O ∉ allowed_ops(T), throw error - Else apply operator using parameter binding ∴ Only type-appropriate operators can be used
const ALLOWED_OPS: Record<string, string[]> = {
String: ['contains', 'startsWith', 'endsWith'],
Int: ['lt', 'lte', 'gt', 'gte']
}
if (!ALLOWED_OPS[fieldType]?.includes(operator)) {
throw new Error('Invalid operator')
} المبرهنة 4: أمان المعرّفات
All SQL identifiers I are validated to prevent control characters and to ensure safe quoting/escaping per dialect.
Identifier processing: 1. Reject if contains control characters 2. Escape internal quote characters per dialect rules 3. Quote identifiers when required (reserved keywords or special characters) 4. Enforce per-dialect identifier length limits ∴ Identifiers cannot break SQL syntax or introduce injected tokens
const quoteIdentifier = (id: string) => {
if (/[\x00-\x1F]/.test(id)) {
throw new Error('Invalid characters')
}
const escaped = id.replace(/"/g, '""')
const needsQuoting = /[^a-z0-9_]/i.test(id) || isReservedKeyword(id)
return needsQuoting ? `"${escaped}"` : id
} المبرهنة 5: اتساق ترتيب المعاملات
For every placeholder position N emitted into the SQL text, params[N-1] contains the exact value intended for that position, with no reordering or mismatch.
Parameter ordering guarantee: 1. A single tracker maintains insertion order 2. Each add() call appends the value to params and immediately emits the next placeholder token 3. SQL text construction and params construction proceed in lockstep 4. No intermediate reordering operations occur ∴ One-to-one correspondence is maintained throughout
class ParameterTracker {
private params: any[] = []
add(value: any): string {
this.params.push(value)
return `$${this.params.length}`
}
getParams(): any[] {
return this.params
}
}
const tracker = new ParameterTracker()
const sql = `WHERE email = ${tracker.add(email)} AND age > ${tracker.add(age)}`
const params = tracker.getParams() تغطية اختبارات أمان شاملة
137 اختبارًا يتحقق من الحماية ضد متجهات الحقن الشائعة وحالات الحواف (المجموعة الحالية)
تعلّيم القيم (basic.test.ts)
- Strings with quotes: user'with'quotes
- Strings with semicolons: user;extra
- SQL keywords as values: DROP TABLE users
- Complex injection: '; DROP TABLE users; --
- Union attacks: ' UNION SELECT * FROM users--
- Boolean attacks: admin' OR '1'='1
- Comment injection: test@example.com' -- comment
التحقق من أسماء الحقول (identifiers.test.ts)
- Reject malicious field names in SELECT
- Reject malicious field names in WHERE
- Reject malicious field names in ORDER BY
- Reject SQL injection in field names
- Reject non-existent fields
- Reject prototype pollution: __proto__, constructor
أمان أنماط LIKE (like-patterns.test.ts)
- Wildcard injection: %' OR '1'='1
- Underscore injection: test_' OR '1'='1
- Backslash handling: test\\'; DROP--
- Multiple wildcards: %_%'; DROP--
- Case insensitive injection: '; UNION SELECT--
أمان عوامل المصفوفات (array-operators.test.ts)
- IN with malicious arrays: ['; DROP--', 'UNION SELECT--']
- NOT IN with injection: ['; TRUNCATE--', 'DELETE FROM--']
- Empty array handling
- Large array validation (100+ items)
- Mixed type array handling
حالات حواف (edge-cases.test.ts)
- Unicode injection: \u0027 OR \u00271\u0027=\u00271
- Hex-encoded injection: 0x31=0x31--
- URL encoded: %27%3B%20DROP%20TABLE
- Stacked queries: '; DROP TABLE users; SELECT
- Time-based blind (dialect-specific): WAITFOR DELAY '00:00:05'--
- UNION-based: UNION ALL SELECT null, password
- Second-order injection attempts
التحقق من ترتيب المعاملات (basic.test.ts)
- Sequential placeholder positions
- Parameter array matches placeholder order
- Complex queries maintain order across conditions
- Nested OR/AND conditions preserve parameter sequence
- Relation filters maintain correct parameter mapping
آمن مقابل غير آمن: مقارنة كود
❌ غير آمن: دمج سلاسل نصية
const email = req.body.email
const sql = "SELECT * FROM users WHERE email = '" + email + "'" ✅ آمن: ربط معاملات تلقائي
const email = req.body.email
const { sql, params } = toSQL('User', 'findMany', {
where: { email }
})
const result = { sql, params } تفاصيل التنفيذ
إدارة المعاملات
تتبّع مركزي للمعاملات يضمن أن كل قيمة مستخدم مربوطة ومستقرة الترتيب
class ParameterTracker {
private params: any[] = []
add(value: any): string {
this.params.push(value)
return `$${this.params.length}`
}
getParams(): any[] {
return this.params
}
} التحقق من الحقول
التحقق المعتمد على المخطط يمنع الوصول التعسفي للحقول
function validateField(
fieldName: string,
model: ModelInfo
): FieldInfo {
const field = model.fields.get(fieldName)
if (!field) {
throw new Error(
`Field "${fieldName}" does not exist`
)
}
return field
} اقتباس المعرّفات
التحقق والاقتباس وفق اللهجة يمنع حقنًا عبر المعرّفات
function quoteIdentifier(identifier: string): string {
if (/[\x00-\x1F]/.test(identifier)) {
throw new Error('Invalid control characters')
}
const escaped = identifier.replace(/"/g, '""')
const needsQuoting = /[^a-z0-9_]/i.test(identifier) || isReservedKeyword(identifier)
if (needsQuoting) {
return `"${escaped}"`
}
return identifier
} أفضل ممارسات الأمان
لا تُعطّل التحقق
لا تتجاوز التحقق من المخطط أو فحص الحقول. هذه طبقات أمان حرجة.
حدّث المخطط باستمرار
تأكد أن مخطط Prisma يعكس بنية قاعدة البيانات بدقة.
تحقق من أنواع الإدخال
TypeScript يوفر أمانًا وقت الترجمة، لكن التحقق وقت التشغيل يزيد الدفاع في العمق.
راقب أنماط الاستعلامات
سجّل وراقب SQL المُولّد والمعاملات في الإنتاج لاكتشاف الشذوذ وسوء الاستخدام.
الأمان بالتصميم
حماية حقن SQL مدمجة في كل طبقة من هذه المكتبة. راجع اختبارات الأمان وتفاصيل التنفيذ مباشرةً.