SQL الديناميكي الآمن: EXECUTE و format
أوامر تُبنى وقت التشغيل: قوة كبيرة، وباب خطير إن لم يُحكم. ⏱ نحو 45 دقيقة.
- تنفّذ أوامر مبنية وقت التشغيل بـ
EXECUTE. - تبني الأمر بـ
formatو %Iو %L. - تشرح حقن
SQLوتجربه. - تمنع الحقن بـ
USINGوقوائم المسموح.
١حين لا تعرف اسم الجدول مسبقًا
الأوامر داخل PL/pgSQL ثابتة: أسماء الجداول والأعمدة مكتوبة في الكود. لكن أحيانًا يأتي الاسم في متغير: «عدّ صفوف كل جدول»، أو «رتّب حسب العمود الذي اختاره المستخدم». هنا نبني الأمر نصًّا وننفّذه بـ EXECUTE:
DO $$DECLARE t text; n bigint;BEGIN FOR t IN SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name LOOP EXECUTE format('SELECT count(*) FROM %I', t) INTO n; RAISE NOTICE '%: % صفًّا', t, n; END LOOP;END $$;
format يبني النص، و%I يضع اسم الجدول بأمان (Identifier)، وINTO n يحفظ النتيجة.
| رمز format | يضع | مثال الناتج |
|---|---|---|
%I | اسم جدول أو عمود (يُحاط بعلامتي " عند الحاجة) | "order items" |
%L | قيمة نصية (تُحاط بعلامتي ' وتُهرَّب) | 'O''Brien' |
%s | نص كما هو بلا حماية | للأرقام الموثوقة فقط |
٢الخطر: حقن SQL
بناء الأمر بلصق النصوص (||) يفتح بابًا خطيرًا: إن كتب المستخدم علامة ' في المدخلات، صار جزءًا من الأمر لا من البيانات:
CREATE FUNCTION find_unsafe(who text) RETURNS bigintLANGUAGE plpgsqlAS $$DECLARE n bigint;BEGIN EXECUTE 'SELECT count(*) FROM customers WHERE name = ''' || who || '''' INTO n; RETURN n;END;$$; SELECT find_unsafe('سارة') AS بحث_عادي, find_unsafe($q$' OR '1'='1$q$) AS بحث_محقون;
| بحث_عادي | بحث_محقون |
|---|---|
| 1 | 8 |
المدخل ' OR '1'='1 حوّل الشرط إلى «صحيح دائمًا» فعدّ كل العملاء. في نظام حقيقي يعني هذا تسريب البيانات، أو حذفها.
العلاج: لا تلصق القيم أبدًا. مرّرها بـ USING (أفضل)، أو ضعها بـ %L:
CREATE FUNCTION find_safe(who text) RETURNS bigintLANGUAGE plpgsqlAS $$DECLARE n bigint;BEGIN EXECUTE 'SELECT count(*) FROM customers WHERE name = $1' INTO n USING who; -- القيمة تُمرَّر منفصلة عن نص الأمر RETURN n;END;$$; SELECT find_safe('سارة') AS بحث_عادي, find_safe($q$' OR '1'='1$q$) AS بحث_محقون;
| بحث_عادي | بحث_محقون |
|---|---|
| 1 | 0 |
المدخل الخبيث صار مجرد اسم يُبحث عنه، فلم يجد شيئًا.
٣القاعدة الذهبية
| ما يتغير | كيف تضعه |
|---|---|
| قيمة (اسم عميل، مدينة، رقم) | EXECUTE … $1 … USING v (الأفضل) أو %L |
| اسم جدول أو عمود | format('… %I …', name)، والأفضل مقارنته بقائمة مسموحة |
| كلمة SQL (ASC أو DESC) | اختر من قائمة ثابتة بـ CASE أو IF، ولا تأخذها من المستخدم مباشرة |
CREATE FUNCTION products_sorted(col text, dir text DEFAULT 'ASC')RETURNS SETOF productsLANGUAGE plpgsqlAS $$BEGIN IF col NOT IN ('name', 'price', 'stock') THEN RAISE EXCEPTION 'عمود ترتيب غير مسموح: %', col; END IF; RETURN QUERY EXECUTE format('SELECT * FROM products ORDER BY %I %s LIMIT 3', col, CASE WHEN upper(dir) = 'DESC' THEN 'DESC' ELSE 'ASC' END);END;$$; SELECT name, price FROM products_sorted('price', 'desc');
| name | price |
|---|---|
| حاسب محمول | 3200 |
| جهاز لوحي | 1450 |
| شاشة 27 بوصة | 980 |
توقّع وتحقّق
أفضل طريقة لتمرير قيمة إلى أمر ديناميكي:
الرمز المناسب لاسم جدول في format:
حقن SQL يحدث حين:
مقابل EXECUTE في Oracle:
الخلاصة
EXECUTEنص_الأمر [INTOمتغير] [USINGقيم].format: %Iللأسماء، و %Lللقيم، و %sبلا حماية.- الحقن: مدخلات تصبح جزءًا من الأمر.
- القيم بـ
USINGدائمًا، والأسماء بـ %Iوقائمة مسموحة.
تحدٍّ برمجي: عدّاد آمن لأي جدول
متقدماكتب دالة count_where(tbl text, col text, val text) تُرجع عدد صفوف الجدول tbl التي يساوي فيها العمود col القيمة val، بأمان من الحقن.
- ترجع
bigint، وتبني الأمر بـformatو%Iلاسمي الجدول والعمود. - القيمة تُمرَّر بـ
USING. - مدخل خبيث في val يُرجع 0، واسم جدول خبيث يفشل دون أن يضر.
👀 اعرض الناتج المطلوب
- الكود يعمل بلا أخطاء
- عملاء الرياض ← 3
- الطلبات المدفوعة ← 5
- قيمة خبيثة ← 0
- اسم جدول خبيث يفشل
- جدول البنود ما زال سليمًا
- format مع %I
- USING
💡 تلميح 1
format('SELECT count(*) FROM %I WHERE %I = $1', tbl, col).💡 تلميح 2
INTO n USING val;حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
CREATE FUNCTION count_where(tbl text, col text, val text) RETURNS bigintLANGUAGE plpgsqlAS $$DECLARE n bigint;BEGIN EXECUTE format('SELECT count(*) FROM %I WHERE %I = $1', tbl, col) INTO n USING val; RETURN n;END;$$;