OWASP 类别: MASVS-CODE:代码质量
概述
SQL 注入通过将代码插入 SQL 语句来利用易受攻击的应用程序,从而访问超出其有意公开的界面的底层数据库。攻击可能会泄露私人数据,破坏数据库内容,甚至危及后端基础设施。
在执行之前,通过连接用户输入动态创建的查询可能会使 SQL 易受注入攻击。SQL 注入通常针对 Web、移动和任何 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;
内容提供器
内容提供器提供了一种结构化的存储机制,可以将其限制为一个应用程序,也可以将其导出以与其他应用共享。应根据最小权限原则设置权限;导出的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()
模式。 - 用于验证列是否在setProjectionMap中允许的
setStrictColumns()
。 - 用于限制子查询的
setStrictGrammar()
。
使用Room库
android.database.sqlite
包提供了在Android上使用数据库所需的API。但是,这种方法需要编写底层代码,并且缺乏对原始SQL查询的编译时验证。随着数据图的变化,需要手动更新受影响的SQL查询——这是一个耗时且容易出错的过程。
一个高级解决方案是使用Room持久性库作为SQLite数据库的抽象层。Room的功能包括:
- 数据库类,作为连接到应用程序持久化数据的入口点。
- 表示数据库表的实体数据。
- 数据访问对象(DAO),提供应用程序可用于查询、更新、插入和删除数据的函数。
Room的优势包括:
- SQL查询的编译时验证。
- 减少易出错的样板代码。
- 简化数据库迁移。
最佳实践
SQL注入是一种强大的攻击,特别是在大型和复杂的应用程序中,很难完全抵御。应该采取额外的安全措施来限制数据接口中潜在缺陷的严重性,包括:
- 强大的单向加盐哈希算法来加密密码。
- 对于商业应用程序,使用256位AES加密。
- 对于椭圆曲线密码学,使用224位或256位公钥大小。
- 限制权限。
- 精确地构建数据格式并验证数据是否符合预期格式。
- 尽可能避免存储个人或敏感用户数据(例如,通过哈希实现应用程序逻辑,而不是传输或存储数据)。
- 尽量减少访问敏感数据的API和第三方应用程序。