إدخال عبر 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 الناجح في أحد موفّري المحتوى إلى منح إذن الوصول إلى أي جداول أخرى.
  • لدى مقدّم المحتوى أذونات متعددة للمحتوى ضمن قاعدة البيانات نفسها.
    • يمكن أن يؤدي إدخال تعليمات برمجية SQL في موفّر محتوى واحد يمنح إذن الوصول بمستويات أذونات مختلفة إلى تجاوز إعدادات الأمان أو الخصوصية محليًا.

التأثير

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

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

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

يؤدي استخدام ? كمعلَمة قابلة للاستبدال في عبارات الاختيار ومجموعة منفصلة من وسيطات الاختيار إلى ربط بيانات أدخلها المستخدم مباشرةً بطلب البحث بدلاً من تفسيرها كجزء من عبارة لغة الاستعلامات البنيوية (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 كطبقة تجريد لقواعد بيانات SQLite. تشمل ميزات الغرفة ما يلي:

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

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

  • التحقّق من صحة طلبات البحث بلغة SQL في وقت الترجمة البرمجية
  • تقليل الرموز النموذجية المعرَّضة للأخطاء
  • نقل قواعد البيانات بسلاسة

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

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

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

الموارد