相信大家对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
):对类字段/容器域敏感
在使用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注入风险的。
若分析引擎可以很好地识别被污染对象的具体成员,方法二的情况不会发生误报,那么其大概率是域敏感的。
非域敏感的分析可能会这么处理:
一个对象的某一个成员被污染了,就把该对象整体当作是一个被污染的对象,后续获取该对象的所有成员都认为是被污染的数据,从而造成误报。
`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
,你爱了吗!