تزریق SQL

رده OWASP: MASVS-CODE: کیفیت کد

نمای کلی

تزریق SQL با وارد کردن کد در دستورات SQL، از برنامه‌های آسیب‌پذیر سوءاستفاده می‌کند تا به پایگاه‌های داده‌ی زیربنایی فراتر از رابط‌های کاربری عمداً در معرض دید آنها دسترسی پیدا کند. این حمله می‌تواند داده‌های خصوصی را افشا کند، محتوای پایگاه داده را خراب کند و حتی زیرساخت‌های backend را به خطر بیندازد.

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 می‌تواند داده‌های حساس کاربر یا برنامه را افشا کند، محدودیت‌های احراز هویت و مجوز را از بین ببرد و پایگاه‌های داده را در معرض خرابی یا حذف قرار دهد. تأثیرات می‌تواند شامل پیامدهای خطرناک و ماندگاری برای کاربرانی باشد که داده‌های شخصی آنها افشا شده است. ارائه‌دهندگان برنامه‌ها و خدمات در معرض خطر از دست دادن مالکیت معنوی یا اعتماد کاربر قرار می‌گیرند.

کاهش‌ها

پارامترهای قابل تعویض

استفاده از ? به عنوان یک پارامتر قابل تعویض در عبارات انتخاب و یک آرایه جداگانه از آرگومان‌های انتخاب، ورودی کاربر را مستقیماً به پرس‌وجو متصل می‌کند، به جای اینکه آن را به عنوان بخشی از یک دستور SQL تفسیر کند.

کاتلین

// 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

جاوا

// 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 در نظر گرفته شود، مستقیماً به کوئری متصل می‌شود و از تزریق کد جلوگیری می‌کند.

در اینجا یک مثال مفصل‌تر آورده شده است که پرس‌وجوی یک برنامه خرید برای بازیابی جزئیات خرید با پارامترهای قابل تعویض را نشان می‌دهد:

کاتلین

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
}

جاوا

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 به عنوان مقدار برای ID خوانده می‌شود، که احتمالاً هیچ نتیجه‌ای نمی‌دهد، در حالی که یک پرس‌وجوی خام، عبارت OR 1=1 را به عنوان بخش دیگری از عبارت WHERE تفسیر می‌کند. مثال زیر یک پرس‌وجوی پارامتری را نشان می‌دهد:

کاتلین

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

جاوا

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

استفاده از روش‌های پرس‌وجو

در این مثال طولانی‌تر، selection و selectionArgs از متد query() برای ایجاد یک عبارت WHERE با هم ترکیب می‌شوند. از آنجایی که آرگومان‌ها به صورت جداگانه ارائه می‌شوند، قبل از ترکیب، escape می‌شوند و از تزریق SQL جلوگیری می‌شود.

کاتلین

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()
}

جاوا

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() برای محدود کردن زیردرخواست‌ها.

از کتابخانه اتاق استفاده کنید

بسته android.database.sqlite رابط‌های برنامه‌نویسی کاربردی (API) لازم برای استفاده از پایگاه‌های داده در اندروید را فراهم می‌کند. با این حال، این رویکرد نیاز به نوشتن کد سطح پایین دارد و فاقد تأیید زمان کامپایل کوئری‌های خام SQL است. با تغییر نمودارهای داده‌ها، کوئری‌های SQL تحت تأثیر باید به صورت دستی به‌روزرسانی شوند - فرآیندی زمان‌بر و مستعد خطا.

یک راه حل سطح بالا، استفاده از کتابخانه Room Persistence به عنوان یک لایه انتزاعی برای پایگاه‌های داده SQLite است. ویژگی‌های Room عبارتند از:

  • یک کلاس پایگاه داده که به عنوان نقطه دسترسی اصلی برای اتصال به داده‌های ذخیره شده برنامه عمل می‌کند.
  • موجودیت‌های داده‌ای که جداول پایگاه داده را نشان می‌دهند.
  • اشیاء دسترسی به داده (DAOs)، که روش‌هایی را ارائه می‌دهند که برنامه می‌تواند برای پرس‌وجو، به‌روزرسانی، درج و حذف داده‌ها از آنها استفاده کند.

مزایای اتاق شامل موارد زیر است:

  • تأیید زمان کامپایل کوئری‌های SQL.
  • کاهش کدهای تکراری مستعد خطا.
  • مهاجرت ساده پایگاه داده.

بهترین شیوه‌ها

تزریق SQL یک حمله‌ی قوی است که به سختی می‌توان در برابر آن کاملاً مقاوم بود، به خصوص در برنامه‌های بزرگ و پیچیده. ملاحظات امنیتی بیشتری باید برای محدود کردن شدت نقص‌های احتمالی در رابط‌های داده در نظر گرفته شود، از جمله:

  • هش‌های قوی، یک‌طرفه و نمکی برای رمزگذاری رمزهای عبور:
    • AES 256 بیتی برای کاربردهای تجاری
    • اندازه‌های کلید عمومی ۲۲۴ یا ۲۵۶ بیتی برای رمزنگاری منحنی بیضوی.
  • محدود کردن مجوزها.
  • ساختاردهی دقیق قالب‌های داده‌ها و تأیید مطابقت داده‌ها با قالب مورد انتظار.
  • در صورت امکان، از ذخیره داده‌های شخصی یا حساس کاربر خودداری کنید (برای مثال، پیاده‌سازی منطق برنامه با استفاده از هش کردن به جای انتقال یا ذخیره داده‌ها).
  • به حداقل رساندن APIها و برنامه‌های شخص ثالثی که به داده‌های حساس دسترسی دارند.

منابع