سؤال

لنفترض أن لدي رمزًا مثل هذا:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

تقول وثائق PDO:

لا يلزم اقتباس معلمات البيانات المعدة؛السائق يتولى الأمر نيابة عنك.

هل هذا حقًا كل ما يجب علي فعله لتجنب حقن SQL؟هل هو حقا بهذه السهولة؟

يمكنك افتراض MySQL إذا كان يحدث فرقًا.أيضًا، أشعر بالفضول حقًا بشأن استخدام البيانات المعدة ضد حقن SQL.في هذا السياق، لا أهتم بـ XSS أو نقاط الضعف المحتملة الأخرى.

هل كانت مفيدة؟

المحلول

الجواب القصير هو لا, ، لن تدافع شركة PDO عن جميع هجمات حقن SQL المحتملة.لبعض حالات الحواف الغامضة.

أنا أتأقلم هذه الإجابة للحديث عن شركة تنمية نفط عمان...

الجواب الطويل ليس بهذه السهولة.إنه يعتمد على الهجوم أظهر هنا.

الهجوم

لذا، دعونا نبدأ بإظهار الهجوم...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

في ظروف معينة، سيؤدي ذلك إلى إرجاع أكثر من صف واحد.دعونا نحلل ما يحدث هنا:

  1. اختيار مجموعة الأحرف

    $pdo->query('SET NAMES gbk');
    

    لكي ينجح هذا الهجوم، نحتاج إلى التشفير الذي يتوقعه الخادم على الاتصال لتشفيره ' كما هو الحال في ASCII أي 0x27 و للحصول على بعض الأحرف التي يكون البايت النهائي لها هو ASCII \ أي. 0x5c.كما اتضح، هناك 5 ترميزات مدعومة في MySQL 5.6 افتراضيًا: big5, cp932, gb2312, gbk و sjis.سوف نختار gbk هنا.

    الآن، من المهم جدًا ملاحظة استخدام SET NAMES هنا.هذا يحدد مجموعة الأحرف على الخادم.هناك طريقة أخرى للقيام بذلك، لكننا سنصل إليها قريبًا.

  2. الحمولة

    تبدأ الحمولة التي سنستخدمها في هذا الحقن بتسلسل البايت 0xbf27.في gbk, ، هذا حرف متعدد البايت غير صالح؛في latin1, ، إنها السلسلة ¿'.لاحظ أن في latin1 و gbk, 0x27 من تلقاء نفسها هو حرفي ' شخصية.

    لقد اخترنا هذه الحمولة لأننا إذا اتصلنا بها addslashes() عليه، سنقوم بإدراج ASCII \ أي. 0x5c, ، قبل ' شخصية.لذلك سوف ننتهي مع 0xbf5c27, ، والتي في gbk عبارة عن تسلسل مكون من حرفين: 0xbf5c تليها 0x27.أو بمعنى آخر أ صالح حرف متبوع بحرف غير مهرب '.لكننا لا نستخدم addslashes().إذن إلى الخطوة التالية...

  3. $stmt->تنفيذ()

    الشيء المهم الذي يجب إدراكه هنا هو أن شركة تنمية نفط عمان تفعل ذلك بشكل افتراضي لا القيام ببيانات معدة بشكل صحيح.إنه يحاكيهم (لـ MySQL).ولذلك، تقوم شركة تنمية نفط عمان ببناء سلسلة الاستعلام داخليًا، واستدعاءها mysql_real_escape_string() (وظيفة MySQL C API) على كل قيمة سلسلة منضمة.

    استدعاء C API ل mysql_real_escape_string() يختلف عن addslashes() من حيث أنه يعرف مجموعة أحرف الاتصال.لذلك يمكنه إجراء الهروب بشكل صحيح لمجموعة الأحرف التي يتوقعها الخادم.ومع ذلك، حتى هذه اللحظة، يعتقد العميل أننا ما زلنا نستخدم latin1 للاتصال، لأننا لم نقول ذلك خلاف ذلك.لقد أخبرنا الخادم كانوا يستخدمون gbk, ، لكن ال عميل لا يزال يعتقد ذلك latin1.

    ولذلك الدعوة إلى mysql_real_escape_string() يُدرج الخط المائل العكسي، ويكون لدينا تعليق مجاني ' الشخصية في المحتوى "الهارب" الخاص بنا!في الواقع، إذا أردنا أن ننظر $var في ال gbk مجموعة الأحرف، سنرى:

    縗' OR 1=1 /*

    وهو بالضبط ما يتطلبه الهجوم.

  4. الاستعلام

    هذا الجزء مجرد إجراء شكلي، ولكن إليك الاستعلام المقدم:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

تهانينا، لقد نجحت للتو في مهاجمة برنامج باستخدام بيانات PDO المعدة مسبقًا...

الإصلاح البسيط

الآن، تجدر الإشارة إلى أنه يمكنك منع ذلك عن طريق تعطيل البيانات المجهزة التي تمت محاكاتها:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

هذا سوف عادة يؤدي إلى بيان معد صحيح (أي.البيانات التي يتم إرسالها في حزمة منفصلة عن الاستعلام).ومع ذلك، كن على علم بأن شركة تنمية نفط عمان سوف تفعل ذلك بصمت تراجع لمحاكاة البيانات التي لا يستطيع MySQL إعدادها محليًا:تلك التي يمكن أن تكون المدرجة في الدليل، ولكن احذر من تحديد إصدار الخادم المناسب).

الإصلاح الصحيح

المشكلة هنا هي أننا لم نستدعي C API mysql_set_charset() بدلاً من SET NAMES.إذا فعلنا ذلك، فسيكون الأمر جيدًا بشرط أن نستخدم إصدار MySQL منذ عام 2006.

إذا كنت تستخدم إصدارًا سابقًا من MySQL، فإن أ حشرة في mysql_real_escape_string() يعني أن الأحرف متعددة البايت غير الصالحة مثل تلك الموجودة في حمولتنا تم التعامل معها على أنها بايتات مفردة لأغراض الهروب حتى لو تم إبلاغ العميل بشكل صحيح بتشفير الاتصال وبالتالي فإن هذا الهجوم سيظل ناجحًا.تم إصلاح الخلل في MySQL 4.1.20, 5.0.22 و 5.1.11.

ولكن الجزء الأسوأ هو ذلك PDO لم يكشف عن واجهة برمجة تطبيقات C لـ mysql_set_charset() حتى 5.3.6، وذلك في الإصدارات السابقة لا تستطيع منع هذا الهجوم لكل أمر ممكن!لقد تم كشفه الآن باعتباره معلمة DSN, ، والتي ينبغي استخدامها بدلاً من SET NAMES...

النعمة المنقذة

كما قلنا في البداية، لكي ينجح هذا الهجوم، يجب تشفير اتصال قاعدة البيانات باستخدام مجموعة أحرف ضعيفة. utf8mb4 يكون ليست ضعيفة وحتى الآن يمكن أن تدعم كل حرف يونيكود:لذا يمكنك اختيار استخدام ذلك بدلاً من ذلك، لكنه لم يكن متاحًا إلا منذ MySQL 5.5.3.البديل هو utf8, ، وهو ايضا ليست ضعيفة ويمكن أن تدعم كامل Unicode الطائرة الأساسية متعددة اللغات.

وبدلاً من ذلك، يمكنك تمكين NO_BACKSLASH_ESCAPES وضع SQL، الذي (من بين أشياء أخرى) يغير تشغيل mysql_real_escape_string().مع تمكين هذا الوضع، 0x27 سيتم استبداله ب 0x2727 بدلا من 0x5c27 وبالتالي عملية الهروب لا تستطيع إنشاء أحرف صالحة في أي من الترميزات الضعيفة حيث لم تكن موجودة من قبل (أي. 0xbf27 مازال 0xbf27 إلخ.) - لذلك سيظل الخادم يرفض السلسلة باعتبارها غير صالحة.ومع ذلك، انظر إجابة @eggyal لثغرة أمنية مختلفة يمكن أن تنشأ من استخدام وضع SQL هذا (وإن لم يكن مع PDO).

أمثلة آمنة

الأمثلة التالية آمنة:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

لأن الخادم يتوقع utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

لأننا قمنا بتعيين مجموعة الأحرف بشكل صحيح بحيث يتطابق العميل والخادم.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

لأننا قمنا بإيقاف تشغيل البيانات المُعدة التي تمت محاكاتها.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

لأننا قمنا بتعيين مجموعة الأحرف بشكل صحيح.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

لأن MySQLi يقوم بإعداد بيانات صحيحة طوال الوقت.

تغليف

اذا أنت:

  • استخدم الإصدارات الحديثة من MySQL (الأحدث 5.1، جميع الإصدارات 5.5، 5.6، إلخ) و معلمة مجموعة أحرف DSN الخاصة بشركة PDO (في PHP ≥ 5.3.6)

أو

  • لا تستخدم مجموعة أحرف ضعيفة لتشفير الاتصال (تستخدم فقط utf8 / latin1 / ascii / إلخ)

أو

  • يُمكَِن NO_BACKSLASH_ESCAPES وضع SQL

أنت آمن بنسبة 100%.

خلاف ذلك، أنت عرضة للخطر على الرغم من أنك تستخدم بيانات PDO المُعدة...

إضافة

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

نصائح أخرى

البيانات المعدة/الاستعلامات ذات المعلمات كافية عمومًا للمنع الترتيب الأول الحقن على هذا البيان*.إذا كنت تستخدم SQL ديناميكيًا غير محدد في أي مكان آخر في التطبيق الخاص بك، فأنت لا تزال عرضة للخطر الترتيب الثاني حقنة.

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

يمكنك تنفيذ هجوم حقن من الدرجة الثانية عندما تتمكن من التسبب في تخزين قيمة في قاعدة بيانات يتم استخدامها لاحقًا كقيمة حرفية في الاستعلام.على سبيل المثال، لنفترض أنك أدخلت المعلومات التالية كاسم مستخدم جديد عند إنشاء حساب على موقع ويب (بافتراض وجود قاعدة بيانات MySQL لهذا السؤال):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

إذا لم تكن هناك قيود أخرى على اسم المستخدم، فسيظل البيان المُعد يتأكد من عدم تنفيذ الاستعلام المضمن أعلاه في وقت الإدراج، وتخزين القيمة بشكل صحيح في قاعدة البيانات.ومع ذلك، تخيل أنه في وقت لاحق يقوم التطبيق باسترداد اسم المستخدم الخاص بك من قاعدة البيانات، ويستخدم تسلسل السلسلة لتضمين تلك القيمة في استعلام جديد.قد تتمكن من رؤية كلمة المرور الخاصة بشخص آخر.نظرًا لأن الأسماء القليلة الأولى في جدول المستخدمين تميل إلى أن تكون مسؤولين، فربما تكون قد تنازلت عن المزرعة للتو.(لاحظ أيضًا:وهذا سبب آخر لعدم تخزين كلمات المرور بنص عادي!)

نرى إذن أن البيانات المعدة كافية لاستعلام واحد، ولكنها كافية في حد ذاتها لا كافية للحماية من هجمات حقن SQL عبر التطبيق بأكمله، لأنها تفتقر إلى آلية لفرض أن جميع عمليات الوصول إلى قاعدة البيانات داخل التطبيق تستخدم تعليمات برمجية آمنة.ومع ذلك، يتم استخدامه كجزء من التصميم الجيد للتطبيق - والذي قد يتضمن ممارسات مثل مراجعة التعليمات البرمجية أو التحليل الثابت، أو استخدام ORM، أو طبقة البيانات، أو طبقة الخدمة التي تحد من SQL الديناميكي - البيانات المعدة نكون الأداة الأساسية لحل مشكلة حقن Sql. إذا اتبعت مبادئ جيدة لتصميم التطبيق، بحيث يتم فصل وصولك إلى البيانات عن بقية برنامجك، يصبح من السهل فرض أو تدقيق أن كل استعلام يستخدم المعلمات بشكل صحيح.في هذه الحالة، يتم منع حقن SQL (الترتيب الأول والثاني) تمامًا.


*اتضح أن MySql/PHP (حسنًا، كانوا) مجرد أغبياء في التعامل مع المعلمات عندما تتضمن أحرفًا واسعة، ولا يزال هناك خطأ نادر الحالة المبينة في إجابة أخرى عالية التصويت هنا التي يمكن أن تسمح للحقن بالمرور عبر استعلام ذي معلمات.

لا، ليسوا كذلك دائماً.

يعتمد ذلك على ما إذا كنت تسمح بوضع إدخال المستخدم داخل الاستعلام نفسه.على سبيل المثال:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

سيكون عرضة لحقن SQL ولن ينجح استخدام البيانات المعدة في هذا المثال، لأن إدخال المستخدم يُستخدم كمعرف، وليس كبيانات.ستكون الإجابة الصحيحة هنا هي استخدام نوع من التصفية/التحقق من الصحة مثل:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

ملحوظة:لا يمكنك استخدام PDO لربط البيانات التي تخرج عن DDL (لغة تعريف البيانات)، أي.هذا لا يعمل:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

السبب وراء عدم نجاح ما سبق هو DESC و ASC غير صحيح بيانات.لا يمكن لشركة تنمية نفط عمان الهروب إلا من أجل بيانات.ثانيا، لا يمكنك حتى وضع ' اقتباسات حوله.الطريقة الوحيدة للسماح بالفرز الذي اختاره المستخدم هي التصفية يدويًا والتحقق من ذلك DESC أو ASC.

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

تعمل الاستعلامات ذات المعلمات عن طريق إرسال التعليمات البرمجية والبيانات بشكل منفصل أبداً يكون من الممكن العثور على ثقب في ذلك.

بالرغم من ذلك، لا يزال من الممكن أن تكون عرضة لهجمات أخرى من نوع الحقن.على سبيل المثال، إذا كنت تستخدم البيانات الموجودة في صفحة HTML، فقد تتعرض لهجمات من نوع XSS.

لا هذا لا يكفي (في بعض الحالات المحددة)!افتراضيًا، تستخدم شركة PDO البيانات المُعدة التي تمت محاكاتها عند استخدام MySQL كمحرك قاعدة بيانات.يجب عليك دائمًا تعطيل البيانات المُعدة التي تمت محاكاتها عند استخدام MySQL وPDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

شيء آخر يجب القيام به دائمًا وهو تعيين التشفير الصحيح لقاعدة البيانات:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

انظر أيضًا هذا السؤال ذو الصلة: كيف يمكنني منع حقن SQL في PHP؟

لاحظ أيضًا أن هذا يتعلق فقط بجانب قاعدة البيانات للأشياء التي لا يزال يتعين عليك مراقبتها بنفسك عند عرض البيانات.على سبيل المثالباستخدام htmlspecialchars() مرة أخرى باستخدام أسلوب الترميز والاقتباس الصحيح.

أنا شخصياً سأقوم دائمًا بتشغيل شكل من أشكال الصرف الصحي على البيانات أولاً حيث لا يمكنك أبدًا الوثوق بإدخال المستخدم، ولكن عند استخدام العناصر النائبة/ربط المعلمة، يتم إرسال البيانات المدخلة إلى الخادم بشكل منفصل إلى بيان SQL ثم يتم ربطها معًا.المفتاح هنا هو أن هذا يربط البيانات المقدمة بنوع معين واستخدام محدد ويزيل أي فرصة لتغيير منطق عبارة SQL.

حتى إذا كنت ستمنع الواجهة الأمامية لحقن SQL، باستخدام عمليات فحص html أو js، فعليك أن تأخذ في الاعتبار أن عمليات فحص الواجهة الأمامية "قابلة للتجاوز".

يمكنك تعطيل js أو تحرير نمط باستخدام أداة تطوير الواجهة الأمامية (المدمجة مع Firefox أو Chrome في الوقت الحاضر).

لذلك، من أجل منع حقن SQL، سيكون من المناسب تطهير الواجهة الخلفية لتاريخ الإدخال داخل وحدة التحكم الخاصة بك.

أود أن أقترح عليك استخدام وظيفة PHP الأصلية filter_input()‎ من أجل تحسين قيم GET وINPUT.

إذا كنت تريد المضي قدمًا في الأمان، بالنسبة لاستعلامات قاعدة البيانات المعقولة، أود أن أقترح عليك استخدام التعبير العادي للتحقق من صحة تنسيق البيانات.سيساعدك preg_match() في هذه الحالة!لكن كن حذرا!محرك Regex ليس خفيفًا جدًا.استخدمه فقط إذا لزم الأمر، وإلا سينخفض ​​أداء التطبيق الخاص بك.

الأمن له تكاليف، ولكن لا تضيعوا أدائك!

مثال سهل:

إذا كنت ترغب في التحقق من ضعف ما إذا كانت قيمة ، يتم استلامها من GET هي رقم ، أقل من 99 إذا (! preg_match ('/[0-9] {1،2}/')) {...} هو ثقيل من

if (isset($value) && intval($value)) <99) {...}

لذا فإن الجواب النهائي هو:"لا!لا تمنع البيانات المعدة لشركة PDO جميع أنواع حقن SQL";ولا يمنع القيم غير المتوقعة، بل فقط التسلسل غير المتوقع

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top