Injeksi SQL

Kategori OWASP: MASVS-CODE: Kualitas Kode

Ringkasan

Injeksi SQL mengeksploitasi aplikasi yang rentan dengan memasukkan kode ke dalam pernyataan SQL untuk mengakses database yang mendasari di luar antarmuka yang sengaja diekspos. Serangan ini dapat mengekspos data pribadi, merusak konten database, dan bahkan membahayakan infrastruktur backend.

SQL bisa menjadi rentan terhadap injeksi melalui kueri yang dibuat secara dinamis dengan menggabungkan input pengguna sebelum dieksekusi. Dengan menargetkan web, seluler, dan aplikasi database SQL apa pun, injeksi SQL biasanya ditampilkan dalam Sepuluh Teratas OWASP dalam kerentanan web. Para penyerang menggunakan teknik ini dalam beberapa pelanggaran profil tingkat tinggi.

Dalam contoh dasar ini, input yang tidak di-escape oleh pengguna ke dalam kotak nomor pesanan dapat disisipkan ke dalam string SQL dan ditafsirkan sebagai kueri berikut:

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

Kode tersebut akan menghasilkan error sintaksis database di konsol web, yang menunjukkan bahwa aplikasi mungkin rentan terhadap injeksi SQL. Mengganti nomor pesanan dengan 'OR 1=1– berarti autentikasi dapat dilakukan karena database mengevaluasi pernyataan ke True, karena satu selalu sama dengan satu.

Demikian pula, kueri ini menampilkan semua baris dari tabel:

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

Penyedia konten

Penyedia konten menawarkan mekanisme penyimpanan terstruktur yang dapat dibatasi pada aplikasi atau diekspor untuk berbagi dengan aplikasi lain. Izin harus ditetapkan berdasarkan prinsip hak istimewa terendah; ContentProvider yang diekspor dapat memiliki satu izin yang ditentukan untuk membaca dan menulis.

Perlu diperhatikan bahwa tidak semua injeksi SQL mengarah pada eksploitasi. Beberapa penyedia konten telah memberi pembaca akses penuh ke database SQLite; kemampuan untuk mengeksekusi kueri arbitrer tidak terlalu menguntungkan. Pola yang dapat mewakili masalah keamanan meliputi:

  • Beberapa penyedia konten berbagi satu file database SQLite.
    • Dalam hal ini, setiap tabel mungkin ditujukan untuk penyedia konten unik. Injeksi SQL yang berhasil di satu penyedia konten akan memberikan akses ke tabel lainnya.
  • Penyedia konten memiliki beberapa izin untuk konten dalam database yang sama.
    • Injeksi SQL di satu penyedia konten yang memberikan akses dengan tingkat izin yang berbeda dapat menyebabkan pengabaian keamanan atau setelan privasi secara lokal.

Dampak

Injeksi SQL dapat mengekspos data pengguna atau aplikasi yang sensitif, mengatasi pembatasan autentikasi dan otorisasi, serta membuat database rentan terhadap kerusakan atau penghapusan. Dampak dapat mencakup implikasi jangka panjang dan berbahaya bagi pengguna yang data pribadinya telah terekspos. Penyedia aplikasi dan layanan berisiko kehilangan kekayaan intelektual atau kepercayaan pengguna.

Mitigasi

Parameter yang dapat diganti

Menggunakan ? sebagai parameter yang dapat diganti dalam klausa pemilihan dan array argumen pemilihan terpisah akan mengikat input pengguna secara langsung ke kueri, bukan menafsirkannya sebagai bagian dari pernyataan 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;

Input pengguna terikat langsung dengan kueri, bukan diperlakukan sebagai SQL, sehingga mencegah injeksi kode.

Berikut adalah contoh yang lebih terperinci yang menunjukkan kueri aplikasi belanja untuk mengambil detail pembelian dengan parameter yang dapat diganti:

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;
}

Menggunakan objek PreparedStatement

Antarmuka PreparedStatement mengompilasi pernyataan SQL sebagai objek yang kemudian dapat dieksekusi secara efisien beberapa kali. PreparedStatement menggunakan ? sebagai placeholder untuk parameter, yang akan membuat upaya injeksi yang dikompilasi berikut tidak efektif:

WHERE id=295094 OR 1=1;

Dalam hal ini, pernyataan 295094 OR 1=1 dibaca sebagai nilai untuk ID, yang kemungkinan tidak akan memberikan hasil, sedangkan kueri mentah akan menafsirkan pernyataan OR 1=1 sebagai bagian lain dari klausa WHERE. Contoh di bawah ini menunjukkan kueri berparameter:

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)

Menggunakan metode kueri

Dalam contoh yang lebih panjang ini, selection dan selectionArgs dari metode query() digabungkan untuk membuat klausa WHERE. Karena argumen diberikan secara terpisah, argumen tersebut akan di-escape sebelum kombinasinya, sehingga mencegah injeksi 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
    );

Menggunakan SQLiteQueryBuilder yang dikonfigurasi dengan benar

Developer dapat lebih melindungi aplikasi dengan menggunakan SQLiteQueryBuilder, class yang membantu mem-build kueri untuk dikirim ke objek SQLiteDatabase. Konfigurasi yang direkomendasikan mencakup:

Menggunakan library Room

Paket android.database.sqlite menyediakan API yang diperlukan untuk menggunakan database di Android. Namun, pendekatan ini memerlukan penulisan kode tingkat rendah dan tidak memiliki verifikasi waktu kompilasi kueri SQL mentah. Saat grafik data berubah, kueri SQL yang terpengaruh harus diupdate secara manual – proses yang memakan waktu dan rentan error.

Solusi tingkat tinggi adalah menggunakan Library Persistensi Room sebagai lapisan abstraksi untuk database SQLite. Fitur Room terdiri dari:

  • Class database yang berfungsi sebagai titik akses utama untuk terhubung ke data persisten aplikasi.
  • Entity data yang mewakili tabel database.
  • Objek akses data (DAO), yang menyediakan metode yang dapat digunakan aplikasi untuk membuat kueri, mengupdate, menyisipkan, dan menghapus data.

Manfaat Room meliputi:

  • Verifikasi waktu kompilasi kueri SQL.
  • Pengurangan kode boilerplate yang rentan mengalami error.
  • Migrasi database yang disederhanakan.

Praktik terbaik

Injeksi SQL adalah serangan kuat dan sulit untuk menjadi kebal sepenuhnya terhadapnya, terutama dengan aplikasi yang besar dan kompleks. Pertimbangan keamanan tambahan harus diterapkan untuk membatasi tingkat keparahan potensi kelemahan dalam antarmuka data, termasuk:

  • Hash yang andal, satu arah, dan ditambahkan salt untuk mengenkripsi sandi:
    • AES 256 bit untuk aplikasi komersial.
    • Ukuran kunci publik 224- atau 256-bit untuk kriptografi kurva eliptik.
  • Membatasi izin.
  • Membuat struktur format data secara akurat dan memverifikasi bahwa data sesuai dengan format yang diharapkan.
  • Menghindari penyimpanan data pengguna yang bersifat pribadi atau sensitif jika memungkinkan (misalnya, menerapkan logika aplikasi dengan hashing, bukan mengirimkan atau menyimpan data).
  • Meminimalkan API dan aplikasi pihak ketiga yang mengakses data sensitif.

Referensi