SQL 插入

OWASP 類別:MASVS-CODE:程式碼品質

總覽

SQL 插入技術會將程式碼插入 SQL 陳述式來存取底層資料庫 (非刻意開放的介面),藉此利用有安全漏洞的應用程式。這項攻擊可能會曝露私人資料、損毀資料庫內容,甚至破壞後端基礎架構。

在執行作業前串連使用者輸入內容,可透過動態方式建立查詢。這種查詢一旦遭人利用,就很容易出現 SQL 插入的情形。SQL 插入通常是 OWASP 的十大網路安全漏洞之一,會將網路、行動裝置和任何 SQL 資料庫應用程式做為目標。在許多引起譁然的違規行為中,我們都發現攻擊者曾運用這項技術。

在這個基本示例中,系統會將使用者在訂單號碼方塊中輸入的未逸出內容插入 SQL 字串中,並解讀為下列查詢:

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

這類程式碼會在網路控制台中產生資料庫語法錯誤,顯示應用程式可能容易出現 SQL 插入的安全漏洞。由於資料庫會將陳述式評估為 True (因為 1 永遠等於 1),所以將訂單號碼替換為 'OR 1=1– 代表可以進行驗證。

同樣地,這項查詢會傳回資料表中的所有列:

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

內容供應器

內容供應器提供結構化的儲存機制,可將內容限制為僅供某個應用程式存取,或匯出以供其他應用程式共用。請注意,權限應依據最低權限原則進行設定;匯出的 ContentProvider 則可具有單一指定的讀取和寫入權限。

值得注意的是,並非所有 SQL 插入情形都會導致應用程式遭到利用。某些內容供應器已授予讀取器存取 SQLite 資料庫的完整權限,因此能執行任意查詢幾乎就沒有什麼好處可言。以下我們說明可能導致安全性問題的模式:

  • 多個內容供應器共用同一個 SQLite 資料庫檔案。
    • 在這種情況下,每個表格都適用於不重複的內容供應器。如果在其中一個內容供應器成功插入 SQL,便會授予其他表格的存取權。
  • 內容供應器對同一資料庫中的內容有多項權限。
    • 在授予不同層級存取權的單一內容供應器中插入 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 陳述式讀取為 ID 的值,因此可能不會產生任何結果,而原始查詢則會將 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)

使用查詢方法

在這個詳細示例中,query() 方法的 selectionselectionArgs 合併組成 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 物件。以下是我們建議的設定:

使用 Room 程式庫

android.database.sqlite 套件提供了在 Android 上使用資料庫所需的 API。不過,這個方法需要編寫低階程式碼,並缺少原始 SQL 查詢的編譯時間驗證。當資料圖表發生變更時,受影響的 SQL 查詢需要經由手動更新,因此相當耗時又容易出錯。

高階解決方案是使用 Room 持續性程式庫做為 SQLite 資料庫的抽象層。Room 的功能包括:

  • 資料庫類別,可用做主要存取點來連結至應用程式的保留資料。
  • 資料實體,用於代表資料庫表格。
  • 資料存取物件 (DAO),提供可讓應用程式用於查詢、更新、插入及刪除資料的方法。

Room 的優點包括:

  • 提供 SQL 查詢的編譯時間驗證。
  • 減少使用容易出錯的樣板程式碼。
  • 簡化資料庫遷移作業。

最佳做法

SQL 插入是一種強大的攻擊行為,十分難以徹底防禦,對大型和複雜的應用程式而言更是如此。為了限制資料介面中潛在漏洞的嚴重程度,您應考量採取額外的安全性措施,包括:

  • 使用強大、單向和加鹽雜湊來加密密碼:
    • 256 位元 AES,適用於商業應用程式。
    • 224 位元或 256 位元公開金鑰大小,適用於橢圓曲線密碼編譯。
  • 限制權限。
  • 精確建構資料格式,並確認資料符合預期格式。
  • 盡可能避免儲存個人或敏感的使用者資料 (例如透過雜湊實作應用程式邏輯,而非藉由傳輸或儲存資料達到此目的)。
  • 盡量減少使用會存取敏感資料的 API 和第三方應用程式。

資源