OWASP 类别:MASVS-CODE:代码质量
概览
SQL 注入通过将代码插入 SQL 语句来利用存在漏洞的应用,以便访问故意公开的接口之外的底层数据库。攻击者可能会泄露私密数据、损坏数据库内容,甚至破坏后端基础架构。
通过在执行前以串联用户输入的手段动态创建的查询,很容易为 SQL 注入代码。对于网站、移动设备以及任何 SQL 数据库应用,SQL 注入通常是 OWASP 十大网络安全漏洞之一。攻击者曾在多项重大违规行为中利用该技术。
在此基本示例中,用户在订单号方框中输入的未转义内容可以插入 SQL 字符串,并被解释为以下查询:
SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1
此类代码会在 Web 控制台中生成数据库语法错误,这表明应用可能容易受到 SQL 注入攻击。将订单号替换为 'OR 1=1–
意味着可以实现身份验证,因为数据库会将相应语句评估为 True
(这是因为 1 始终等于 1)。
同样,此查询会返回表格中的所有行:
SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;
content provider
content provider 提供结构化存储机制,可以将内容限制为仅供某个应用访问,也可以将内容导出以与其他应用共享。权限设置应基于最小权限原则;导出的 ContentProvider
可以具有单个指定的读取和写入权限。
值得注意的是,并非所有 SQL 注入都会导致漏洞遭到利用。某些 content provider 已授予读取器对 SQLite 数据库的完全访问权限;能够执行任意查询几乎没有任何好处。可能代表存在安全问题的模式包括:
- 多个 content provider 共享一个 SQLite 数据库文件。
- 在这种情况下,每个表格都可能适用于一个唯一的 content provider。如果在一个 content provider 中成功实现 SQL 注入,则会授予对所有其他表格的访问权限。
- content provider 对同一数据库中的内容拥有多项权限。
- 在授予不同权限级别的访问权限的单个 content provider 中实现 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 和第三方应用。