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()
方法的 selection
和 selectionArgs
合併組成 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
物件。以下是我們建議的設定:
setStrict()
模式,用於查詢驗證。setStrictColumns()
,用於驗證資料欄是否允許列於 setProjectionMap 中。setStrictGrammar()
,用於限制子查詢。
使用 Room 程式庫
android.database.sqlite
套件提供了在 Android 上使用資料庫所需的 API。不過,這個方法需要編寫低階程式碼,並缺少原始 SQL 查詢的編譯時間驗證。當資料圖表發生變更時,受影響的 SQL 查詢需要經由手動更新,因此相當耗時又容易出錯。
高階解決方案是使用 Room 持續性程式庫做為 SQLite 資料庫的抽象層。Room 的功能包括:
- 資料庫類別,可用做主要存取點來連結至應用程式的保留資料。
- 資料實體,用於代表資料庫表格。
- 資料存取物件 (DAO),提供可讓應用程式用於查詢、更新、插入及刪除資料的方法。
Room 的優點包括:
- 提供 SQL 查詢的編譯時間驗證。
- 減少使用容易出錯的樣板程式碼。
- 簡化資料庫遷移作業。
最佳做法
SQL 插入是一種強大的攻擊行為,十分難以徹底防禦,對大型和複雜的應用程式而言更是如此。為了限制資料介面中潛在漏洞的嚴重程度,您應考量採取額外的安全性措施,包括:
- 使用強大、單向和加鹽雜湊來加密密碼:
- 256 位元 AES,適用於商業應用程式。
- 224 位元或 256 位元公開金鑰大小,適用於橢圓曲線密碼編譯。
- 限制權限。
- 精確建構資料格式,並確認資料符合預期格式。
- 盡可能避免儲存個人或敏感的使用者資料 (例如透過雜湊實作應用程式邏輯,而非藉由傳輸或儲存資料達到此目的)。
- 盡量減少使用會存取敏感資料的 API 和第三方應用程式。