likes
comments
collection
share

SAST-短小精悍的Benchmark

作者站长头像
站长
· 阅读数 3

引言

相信大家对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注入风险的,因为在方法二中,usernamesql拼接前被重新赋值为"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,你爱了吗!

github仓库

所有测试样例都会同步到开源仓库,欢迎大家多多star~ github.com/HaHarden/CP…

公主号推荐

公主号id:CodeAnalyzer,公主号名称:CodeAnalyzer Ultra