从用户控制的源构建的查询¶
ID: java/sql-injection
Kind: path-problem
Security severity: 8.8
Severity: error
Precision: high
Tags:
- security
- external/cwe/cwe-089
- external/cwe/cwe-564
Query suites:
- java-code-scanning.qls
- java-security-extended.qls
- java-security-and-quality.qls
如果使用字符串连接构建数据库查询,并且连接的组件包含用户输入,则用户很可能能够运行恶意数据库查询。这适用于各种数据库查询语言,包括 SQL 和 Java 持久化查询语言。
建议¶
通常,最好使用 SQL 预处理语句,而不是使用字符串连接构建完整的 SQL 查询。预处理语句可以包括通配符(写为问号 (?)),用于 SQL 查询的每个部分,预计每次运行时都会用不同的值填充该部分。稍后执行查询时,必须为查询中的每个通配符提供一个值。
在 Java 持久化查询语言中,最好使用带参数的查询,而不是使用字符串连接构建完整的查询。Java 持久化查询可以为查询的每个部分包含一个参数占位符,预计运行时会用不同的值填充该部分。参数占位符可以用冒号 (:) 后跟参数名称,或问号 (?) 后跟整数位置来表示。稍后执行查询时,必须使用 setParameter
方法为查询中的每个参数提供一个值。使用 @NamedQuery
注释指定查询会引入一个额外的安全级别:查询必须是常量字符串文字,防止通过字符串连接进行构造,并且填充查询部分值的唯一方法是设置位置参数。
为查询提供参数值时,最好使用预处理语句(在 SQL 中)或查询参数(在 Java 持久化查询语言中),无论任何参数是否直接可追溯到用户输入。这样做可以避免任何关于引用和转义的担忧。
示例¶
在以下示例中,代码以两种不同的方式运行一个简单的 SQL 查询。
第一种方法涉及通过将环境变量与一些字符串文字连接来构建查询 query1
。环境变量可以包含特殊字符,因此此代码允许 SQL 注入攻击。
第二种方法展示了良好的实践,涉及构建一个查询,query2
,其中包含一个包含通配符 (?
) 的单字符串文字。然后通过调用 setString
为通配符赋予一个值。此版本不受注入攻击的影响,因为环境变量中的任何特殊字符都不会得到任何特殊处理。
{
// BAD: the category might have SQL special characters in it
String category = System.getenv("ITEM_CATEGORY");
Statement statement = connection.createStatement();
String query1 = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
+ category + "' ORDER BY PRICE";
ResultSet results = statement.executeQuery(query1);
}
{
// GOOD: use a prepared query
String category = System.getenv("ITEM_CATEGORY");
String query2 = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=? ORDER BY PRICE";
PreparedStatement statement = connection.prepareStatement(query2);
statement.setString(1, category);
ResultSet results = statement.executeQuery();
}
示例¶
以下代码展示了运行 Java 持久性查询的几种不同方法。
第一个示例涉及构建一个查询,query1
,通过将环境变量与一些字符串文字连接起来。就像 SQL 示例一样,环境变量可以包含特殊字符,因此此代码允许 Java 持久性查询注入攻击。
其余示例演示了使用用户提供的值安全构建 Java 持久性查询的不同方法
query2
使用一个单字符串文字,其中包含一个参数占位符,由冒号 (:
) 和参数名称 (category
) 指示。query3
使用一个单字符串文字,其中包含一个参数占位符,由问号 (?
) 和位置号 (1
) 指示。namedQuery1
使用@NamedQuery
注释进行定义,其query
属性是一个字符串文字,其中包含一个参数占位符,由冒号 (:
) 和参数名称 (category
) 指示。namedQuery2
是使用@NamedQuery
注释定义的,其query
属性包括一个参数占位符,由问号 (?
) 和位置编号 (1
) 表示。然后通过调用setParameter
为参数赋予一个值。这些版本不受注入攻击的影响,因为环境变量或用户提供的值中的任何特殊字符都不会得到任何特殊处理。
{
// BAD: the category might have Java Persistence Query Language special characters in it
String category = System.getenv("ITEM_CATEGORY");
Statement statement = connection.createStatement();
String query1 = "SELECT p FROM Product p WHERE p.category LIKE '"
+ category + "' ORDER BY p.price";
Query q = entityManager.createQuery(query1);
}
{
// GOOD: use a named parameter and set its value
String category = System.getenv("ITEM_CATEGORY");
String query2 = "SELECT p FROM Product p WHERE p.category LIKE :category ORDER BY p.price"
Query q = entityManager.createQuery(query2);
q.setParameter("category", category);
}
{
// GOOD: use a positional parameter and set its value
String category = System.getenv("ITEM_CATEGORY");
String query3 = "SELECT p FROM Product p WHERE p.category LIKE ?1 ORDER BY p.price"
Query q = entityManager.createQuery(query3);
q.setParameter(1, category);
}
{
// GOOD: use a named query with a named parameter and set its value
@NamedQuery(
name="lookupByCategory",
query="SELECT p FROM Product p WHERE p.category LIKE :category ORDER BY p.price")
private static class NQ {}
...
String category = System.getenv("ITEM_CATEGORY");
Query namedQuery1 = entityManager.createNamedQuery("lookupByCategory");
namedQuery1.setParameter("category", category);
}
{
// GOOD: use a named query with a positional parameter and set its value
@NamedQuery(
name="lookupByCategory",
query="SELECT p FROM Product p WHERE p.category LIKE ?1 ORDER BY p.price")
private static class NQ {}
...
String category = System.getenv("ITEM_CATEGORY");
Query namedQuery2 = entityManager.createNamedQuery("lookupByCategory");
namedQuery2.setParameter(1, category);
}
参考资料¶
OWASP:SQL 注入预防备忘单。
SEI CERT Java Oracle 编码标准:IDS00-J. 防止 SQL 注入。
Java 教程:使用已准备好的语句。
Java EE 教程:Java 持久性查询语言。
通用弱点枚举:CWE-89。
通用弱点枚举:CWE-564。