SQL 插入

Stay organized with collections Save and categorize content based on your preferences.

總覽

SQL 插入透過插入程式碼至 SQL 陳述式,利用安全漏洞應用程式,在有意公開的介面之外存取底層資料庫。攻擊者可能會公開私人資料、損毀資料庫內容,甚至破壞後端基礎架構。

SQL 很容易透過查詢插入,該查詢在執行作業前由串連使用者輸入內容動態建立。針對網路、行動裝置和任何 SQL 資料庫應用程式,SQL 插入通常位於網路安全漏洞的 OWASP Top 十。攻擊者已在多個高效設定檔區塊中運用這項技術。

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

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

這類程式碼會在網路控制台中產生資料庫語法錯誤,顯示應用程式可能容易受到 SQL 植入攻擊。將訂單號碼替換為「OR 1=1」代表可以進行驗證,因為資料庫會評估 True 的陳述式,因為 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),提供應用程式可用於查詢、更新、插入及刪除資料的方法。

聊天室的優點包括:

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

最佳做法

SQL 插入是一種強大攻擊,難以完全保持彈性,特別是對大型和複雜的應用程式而言。應採取額外的安全性考量,以限制資料介面中潛在安全漏洞的嚴重程度,包括:

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

資源