OWASP 类别: MASVS-CODE:代码质量
概述
SQL 注入利用易受攻击的应用程序,通过将代码插入 SQL 语句来访问基础数据库,超出其有意公开的接口。攻击可能会泄露私人数据、损坏数据库内容,甚至危及后端基础设施。
SQL 可能会通过在执行前连接用户输入动态创建的查询而容易受到注入攻击。SQL 注入通常针对 Web、移动和任何 SQL 数据库应用程序,通常出现在 OWASP 十大安全漏洞 中。攻击者在一些备受瞩目的安全漏洞中使用了这种技术。
在此基本示例中,用户在订单号框中输入未转义的输入,可以将其插入 SQL 字符串并解释为以下查询
SELECT * FROM users WHERE email = '[email protected]' AND order_number = '251542'' LIMIT 1
此类代码将在 Web 控制台中生成数据库语法错误,这表明应用程序可能容易受到 SQL 注入的攻击。将订单号替换为 'OR 1=1–
表示可以实现身份验证,因为数据库将语句评估为 True
,因为 1 始终等于 1。
类似地,此查询返回表中的所有行
SELECT * FROM purchases WHERE email='[email protected]' 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 和第三方应用程序。