تزریق 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 ایجاد کنند. از آنجایی که آرگومان ها به طور جداگانه ارائه می شوند، قبل از ترکیب آنها فرار می کنند و از تزریق 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 Library به عنوان یک لایه انتزاعی برای پایگاه های داده SQLite است. امکانات اتاق عبارتند از:

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

مزایای اتاق عبارتند از:

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

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

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

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

منابع