إدخال عبر SQL

فئة OWASP: MASVS-CODE: جودة الرموز البرمجية

نظرة عامة

تستغل هجمات SQL Injection التطبيقات المعرّضة للاختراق عن طريق إدخال رمز في عبارات SQL للوصول إلى قواعد البيانات الأساسية خارج واجهاتها المعروضة عن قصد. يمكن أن يؤدي الهجوم إلى الكشف عن البيانات الخاصة وإتلاف محتوى قاعدة البيانات، وحتى اختراق البنية الأساسية للخلفية.

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

في هذا المثال الأساسي، يمكن إدراج إدخال لم يتم تشفيره من قِبل المستخدم في مربّع رقم الطلب في سلسلة SQL وتفسيره على أنّه طلب البحث التالي:

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

سيؤدي هذا الرمز البرمجي إلى حدوث خطأ في بنية قاعدة البيانات في وحدة تحكّم الويب، ما يشير إلى أنّ التطبيق قد يكون معرّضًا لهجوم حقن SQL. يعني استبدال رقم الطلب بـ 'OR 1=1– أنّه يمكن إجراء المصادقة لأنّ قاعدة البيانات تقيّم العبارة على أنّها True، لأنّ واحدًا يساوي واحدًا دائمًا.

وبالمثل، يعرض هذا الاستعلام جميع الصفوف من جدول:

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

موفّرو المحتوى

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

تجدر الإشارة إلى أنّ بعض عمليات حقن SQL لا تؤدّي إلى استغلال. يمنح بعض مقدّمي المحتوى القرّاء إذن الوصول الكامل إلى قاعدة بيانات SQLite، وبالتالي لا يقدّم لهم تنفيذ طلبات بحث عشوائية أي ميزة. تشمل الأنماط التي يمكن أن تمثّل مشكلة أمان ما يلي:

  • مزوّدو محتوى متعدّدون يشاركون ملف قاعدة بيانات SQLite واحدًا
    • في هذه الحالة، قد يكون كل جدول مخصّصًا لمقدّم محتوى فريد. سيؤدي نجاح هجوم SQL Injection في مقدّم محتوى واحد إلى منح إذن الوصول إلى أي جداول أخرى.
  • يمتلك مقدّم المحتوى أذونات متعددة للمحتوى ضمن قاعدة البيانات نفسها.
    • يمكن أن تؤدي عملية حقن SQL في مقدّم محتوى واحد يمنح إذن الوصول بمستويات أذونات مختلفة إلى تجاوز إعدادات الأمان أو الخصوصية على الجهاز.

التأثير

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

إجراءات التخفيف

المَعلمات القابلة للاستبدال

يؤدي استخدام ? كمَعلمة قابلة للاستبدال في عبارات الاختيار ومجموعة منفصلة من وسيطات الاختيار إلى ربط إدخال المستخدم مباشرةً بطلب البحث بدلاً من تفسيره كجزء من عبارة SQL.

Kotlin

// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"

// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")

// Adds values to the selection arguments array.
selectionArgs[0] = userInput

Java

// Constructs a selection clause with a replaceable parameter.
String selectionClause =  "var = ?";

// Sets up an array of arguments.
String[] selectionArgs = {""};

// Adds values to the selection arguments array.
selectionArgs[0] = userInput;

يتم ربط إدخال المستخدم مباشرةً بطلب البحث بدلاً من التعامل معه على أنّه طلب بحث SQL، ما يمنع حقن الرموز.

في ما يلي مثال أكثر تفصيلاً يعرض طلب بحث لتطبيق تسوّق لاسترداد تفاصيل الشراء باستخدام مَعلمات قابلة للاستبدال:

Kotlin

fun validateOrderDetails(email: String, orderNumber: String): Boolean {
    val cursor = db.rawQuery(
        "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
        arrayOf(email, orderNumber)
    )

    val bool = cursor?.moveToFirst() ?: false
    cursor?.close()

    return bool
}

Java

public boolean validateOrderDetails(String email, String orderNumber) {
    boolean bool = false;
    Cursor cursor = db.rawQuery(
      "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?", 
      new String[]{email, orderNumber});
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            bool = true;
        }
        cursor.close();
    }
    return bool;
}

استخدام عناصر PreparedStatement

تُعدّ واجهة PreparedStatement عبارات SQL مسبقًا كعنصر يمكن تنفيذه بعد ذلك بكفاءة عدة مرات. يستخدم PreparedStatement الرمز ? كعنصر نائب للمَعلمات، ما يجعل محاولة الحقن المجمّعة التالية غير فعّالة:

WHERE id=295094 OR 1=1;

في هذه الحالة، تتم قراءة عبارة 295094 OR 1=1 على أنّها قيمة المعرّف، ما قد يؤدي على الأرجح إلى عدم عرض أي نتائج، في حين سيفسر طلب البحث الأوّلي عبارة OR 1=1 على أنّها جزء آخر من العبارة WHERE. يوضّح المثال أدناه طلب بحث يتضمّن معلَمة:

Kotlin

val pstmt: PreparedStatement = con.prepareStatement(
        "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
    setString(1, "Barista")
    setInt(2, 295094)
}

Java

PreparedStatement pstmt = con.prepareStatement(
                                "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")   
pstmt.setInt(2, 295094)

استخدام طُرق طلبات البحث

في هذا المثال الأطول، يتم دمج selection وselectionArgs من طريقة query() لإنشاء عبارة WHERE. وبما أنّ الوسيطات يتم تقديمها بشكل منفصل، يتم ترميزها قبل دمجها، ما يمنع إدخال رموز SQL.

Kotlin

val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
)

// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")

// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"

val cursor = db.query(
    FeedEntry.TABLE_NAME,  // The table to query
    projection,            // The array of columns to return
                           //   (pass null to get all)
    selection,             // The columns for the WHERE clause
    selectionArgs,         // The values for the WHERE clause
    null,                  // Don't group the rows
    null,                  // Don't filter by row groups
    sortOrder              // The sort order
).use {
    // Perform operations on the query result here.
    it.moveToFirst()
}

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
};

// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

استخدام SQLiteQueryBuilder الذي تم ضبطه بشكل صحيح

يمكن للمطوّرين تعزيز حماية التطبيقات باستخدام SQLiteQueryBuilder، وهي فئة تساعد في إنشاء طلبات بحث لإرسالها إلى عناصر SQLiteDatabase. تشمل الإعدادات المقترَحة ما يلي:

  • وضع setStrict() للتحقّق من صحة الطلب
  • setStrictColumns() للتحقّق من أنّ الأعمدة مُدرَجة في القائمة المسموح بها في setProjectionMap
  • setStrictGrammar() لتقييد طلبات البحث الفرعية

استخدام مكتبة Room

توفّر حزمة android.database.sqlite واجهات برمجة التطبيقات اللازمة لاستخدام قواعد البيانات على Android. ومع ذلك، تتطلّب هذه الطريقة كتابة رمز برمجي من المستوى الأدنى ولا تتضمّن عملية التحقّق من استعلامات SQL الأوّلية في وقت الترجمة. مع تغيُّر الرسوم البيانية للبيانات، يجب تعديل استعلامات SQL المتأثرة يدويًا، وهي عملية تستغرق وقتًا طويلاً وتكون عرضة للخطأ.

من بين الحلول العالية المستوى استخدام مكتبة Room Persistence Library كطبقة تجريدية لقواعد بيانات SQLite. تشمل ميزات الغرفة ما يلي:

  • فئة قاعدة بيانات تُستخدَم كنقطة الوصول الرئيسية للربط ببيانات التطبيق الثابتة
  • عناصر البيانات التي تمثّل جداول قاعدة البيانات
  • عناصر الوصول إلى البيانات (DAO)، التي توفّر طرقًا يمكن للتطبيق استخدامها لاستعلام البيانات وتعديلها وإدراجها وحذفها

تشمل مزايا الغرفة ما يلي:

  • التحقّق من طلبات لغة الاستعلامات البنيوية (SQL) في وقت الترجمة
  • تقليل الرموز النموذجية المعرّضة للخطأ
  • نقل قاعدة البيانات بشكلٍ سلس

أفضل الممارسات

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

  • تجزئات قوية أحادية الاتجاه ومضاف إليها قيمة عشوائية للحماية من أجل تشفير كلمات المرور:
    • ‫AES بسعة 256 بت للتطبيقات التجارية
    • أحجام المفاتيح العامة التي تبلغ 224 أو 256 بت لتشفير المنحنى الإهليلجي
  • تقييد الأذونات
  • تنظيم تنسيقات البيانات بدقة والتأكّد من توافق البيانات مع التنسيق المتوقّع
  • تجنُّب تخزين بيانات المستخدمين الشخصية أو الحسّاسة كلّما أمكن (على سبيل المثال، تنفيذ منطق التطبيق من خلال التجزئة بدلاً من نقل البيانات أو تخزينها)
  • الحدّ من عدد واجهات برمجة التطبيقات والتطبيقات التابعة لجهات خارجية التي يمكنها الوصول إلى البيانات الحسّاسة

المراجع