公主号推荐
公主号id:CodeAnalyzer
,公主号名称:CodeAnalyzer Ultra
github仓库
所有测试样例都会同步到开源仓库
,欢迎大家多多star~
https://github.com/HaHarden/CPGPractise
引言
相信大家对DevSecOps这个概念已经不陌生了,其是一种软件开发理念,强调在DevOps的过程中融入安全性,即将安全实践融入到开发、测试和部署的全生命周期中。DevSecOps的目标是在快速迭代的软件交付过程中,通过自动化工具和持续监控的方式,确保安全措施的有效实施,从而降低安全风险,实现安全左移,提高软件质量和可靠性。
静态代码分析工具自然就成为了开发流程中不可或缺的一部分。熟悉SAST工具的朋友们可能都知道,其最头疼的问题就是漏报/误报问题,SAST工具扫描出来的结果还需要具备安全+开发能力的人员去审计。因为在用户看来,可能很多问题都是误报,这样就增加了SAST工具的使用成本。若SAST工具的分析能力足够强大,将会很大程度上降低其维护成本。
影响SAST工具核心分析能力的是敏感性分析,包括流敏感、路径敏感、域敏感、上下文敏感等。接下来,我们结合具体的代码示例来看这几个敏感性概念。
一、流敏感
流敏感(Flow Sensitive
):对语句的执行顺序敏感
也就是说,引擎是可以感知语句的执行顺序的,若执行顺序改变影响到了最终的分析结果,引擎也是可以感知的。
- 示例
// 方法一
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
String username;
username = "SASTing"; // 赋值在username被污染前执行
// 用户请求中的数据认为是有风险的,所以被认为是污染源
username = req.getParameter("name"); // tainted source
// 污染源被拼接到sql语句中
String sql = "select * from users where username = " + username; // add username to sql
// 模拟sql语句执行,被污染的sql被执行了,所以是被认为是爆发点
executeSql(sql); // sink
}
// 方法二
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
String username;
username = req.getParameter("name");
username = "SASTing"; // 赋值在username被污染后执行
String sql = "select * from users where username = " + username;
executeSql(sql);
}
对比以上两个方法,显然方法一是有SQL注入风险的,方法二是没有SQL注入风险的,因为在方法二中,username
在sql拼接前被重新赋值为"SASTing"
。
若SAST工具支持流敏感分析,那么其是可以感知语句间的执行顺序的,也就是说,可以感知username
被污染和被重新赋值的执行顺序,所以在方法二中不会产生误报;相反,不支持流敏感分析的就会有误报产生。
流不敏感的分析可能会这么处理:
针对变量
username
,只要方法内有一条语句对其进行了污染(即使在污染后对username
做了重新赋值),那么在当前方法内,username
就被认为是污点数据, 从而造成误报。
二、路径敏感
路径敏感(Path Sensitive
):对控制流分支敏感
// 方法一
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// bad
String input;
int x = 3;
// path-sensitive
if (x > 0) {
input = req.getParameter("input1"); // source
} else {
input = "safe input";
}
resp.getWriter().write(input); // sink
}
// 方法二
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// good
String input;
int x = 3;
// path-sensitive
if (x < 0) {
input = req.getParameter("input1");
} else {
input = "safe input";
}
resp.getWriter().write(input);
}
上述两个方法,数据流是这样的:
- source:从用户请求中获取参数"input1"(tainted data)
- sink:tainted data 传递给
resp.getWriter().write()
方法,被直接返回给前端页面 - 因此会有反射型XSS风险
但是,很明显方法二是没有风险的,因为方法二中的if
条件永远为false
,所以input
永远为"safe input"
字符串,所以不存在风险。
若分析引擎可以识别方法二中的分支条件,不会造成误报,那么大概率可以判断是路径敏感(path-sensitive)的。
路径不敏感的分析可能会这么处理:
if 块和 else 块内的数据都流向数据读取处,然后对数据做类似这样的merge处理:merge(unsafeData, safeData) = unsafeData,从而造成误报。
路径敏感一般的实现方式有:常量传播、抽象解释、符号执行、**SSA(静态单赋值)**等。
三、域敏感
域敏感(Field Sensitive
):对类字段/容器域敏感
1.类字段敏感
在使用JDBC+mysql时可能会有如下代码场景:
static final String JDBC_URL = "jdbc:mysql://localhost:3306/mydatabase";
static final String USERNAME = "username";
static final String PASSWORD = "password";
@Data
private static class User {
private String name;
private String gender;
}
// 方法一
private void test_bad(HttpServletRequest req) throws SQLException, ClassNotFoundException {
Connection connection = null;
// 1. 加载驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立数据库连接
connection = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
// 3. 创建Statement对象
Statement statement = connection.createStatement();
String username = req.getParameter("name"); // tainted source
User user = new User();
user.setName(username);
// 4. 执行查询
String query = "SELECT * FROM my_table where name = " + user.getName(); // field-sensitive -> user.getName() tainted
ResultSet resultSet = statement.executeQuery(query); // sink
// 6. 关闭资源
connection.close();
}
// 方法二
private void test_good(HttpServletRequest req) throws ClassNotFoundException, SQLException {
Connection connection = null;
// 1. 加载驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立数据库连接
connection = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
// 3. 创建Statement对象
Statement statement = connection.createStatement();
String username = req.getParameter("name"); // tainted source
User user = new User();
user.setName(username);
// 4. 执行查询
String query = "SELECT * FROM my_table where name = " + user.getGender(); // field-sensitive -> user.getGender() not tainted
ResultSet resultSet = statement.executeQuery(query); // not sink
// 6. 关闭资源
connection.close();
}
可以看到,以上方法一和方法二的主要区别在于:
方法一:user
对象的name
字段被污染,然后将user.getName()
拼接到sql语句
方法二:user
对象的name
字段被污染,然后将user.getGender()
拼接到sql语句
显然,方法二是没有sql注入风险的。
若分析引擎可以很好地识别被污染对象的具体字段,方法二的情况不会发生误报,那么其大概率是域敏感的。
非域敏感的分析可能会这么处理:
一个对象的某一个字段被污染了,就把该对象整体当作是一个被污染的对象,后续获取该对象的所有字段都认为是被污染的数据,从而造成误报。
2.容器域敏感
- 代码片段一
List<String> usernames = new LinkedList<>();
String username = req.getParameter("name"); // tainted source
usernames.add("zhangsan");
usernames.add(username); // passthrough
String query = "SELECT * FROM my_table where name = " + usernames.get(1); // usernames.get(1) is tainted
ResultSet resultSet = statement.executeQuery(query); // sink
- 代码片段二
List<String> usernames = new LinkedList<>();
String username = req.getParameter("name"); // tainted source
usernames.add("zhangsan");
usernames.add(username); // passthrough
String query = "SELECT * FROM my_table where name = " + usernames.get(0); // usernames.get(0) is not tainted
ResultSet resultSet = statement.executeQuery(query); // not sink
可以看到,两个代码片段的区别:
被污染的username
被加到了usernames
的第二个位置
- 代码片段一将
usernames.get(1)
拼接到sql中,然后执行 - 代码片段二将
usernames.get(0)
拼接到sql中,然后执行
显然,片段二是不会造成SQL注入风险的。
若分析引擎能够很好地支持这种容器相关的污点数据流分析,分析片段二时不会误报,那么其大概率是域敏感的。
非域敏感的分析可能会这么处理:
一个容器中的某一个对象被污染了,就把该容器整体当作是一个被污染的容器,后续从容器内获取的所有对象都是被污染的对象,从而造成误报。
四、上下文敏感
上下文敏感(Context Sensitive
):对方法调用的上下文敏感
还是拿JDBC样例举例,就不再写重复的代码了,测试代码如下:
- 共用方法
private static String getName(int x, HttpServletRequest req) {
// 这里也是需要 path-sensitive 路径敏感 能力的
if (x > 0) {
return req.getParameter("name");
}
else {
return "zhangsan";
}
}
- 代码片段一:
String username = getName(1, req);
String query = "SELECT * FROM my_table where name = " + username; // 4. 执行查询
statement.executeQuery(query); // sink
- 代码片段二
String username = getName(-1, req);
String query = "SELECT * FROM my_table where name = " + username; // 4. 执行查询
statement.executeQuery(query); // sink
观察上述代码可以发现:
- 片段一获取的
username
是被污染的数据 - 片段二获取的
username
是安全的数据
二者获取的最终数据是否被污染是由传入方法getName(int x, HttpServletRequest req)
的参数x
决定的,也就是由getName()
方法调用的上下文决定的。
若分析引擎可以很好地识别方法调用的上下文,那么其大概率是上下文敏感的。
上下文不敏感的分析可能这么处理:
在遇到方法调用时,不考虑方法调用的上下文信息,上述示例中可能将
getName()
方法的返回值通过某种规则认为其返回值永远是tainted data或者是safe data,从而造成误报或漏报。
总结
若你想要测试一款SAST分析引擎的分析能力,直接拿本文的测试样例去跑,根据测试结果就能大概评估引擎的分析能力了,这么短小精悍的Benchmark
,你爱了吗!