本文由
创作,已纳入「FreeBuf原创奖励计划」,未授权禁止转载
简介
semgrep 是一款由Facebook开源的白盒代码扫描工具,项目地址:https://github.com/returntocorp/semgrep,其规则编写简单,易用,扫描速度快。相较于CodeQL 而言,入门门槛较低,编写规则简单,且非常方便地接入到CI流程中。
安装步骤
方式一、mac机器上可使用 homebrew 安装:
brew install semgrep
方式二、Ubuntu / Windows via WSL / Linux / macOS 也可以使用pip进行安装:
python3 -m pip install semgrep
方式三、Docker部署:
docker pull returntocorp/semgrep:latest
更多详细内容可参考官方文档:https://semgrep.dev/docs/getting-started/
使用体验
若想使用docker 开箱即用先来体验下效果,这里需要注意的是官方文档中的docker扫描命令需要自行添加-v ,把宿主机上的源代码文件挂载到docker 中:
以扫描WebGoat 为例:
$ git clone https://github.com/WebGoat/WebGoat
$ docker run -v "${PWD}:/src" returntocorp/semgrep:latest --config p/security-audit WebGoat/webgoat-lessons-o scan.txt--json --time
-o:输出扫描结果到文件,可以输出json格式/xml/sarif 等格式
--config: 配置扫描规则文件 官方也提供了一些规则文件,在https://semgrep.dev/r里可以查看各种分类的规则集。以sql 注入的规则为例,关键字搜索sql,可以看到sql-injection 这个规则集就提供了多达了37条规则。

如果想使用这个规则集来扫描的话,可以添加 --config "p/sql-injection",利用这个规则集我们发现了WebGoat存在8处sql 注入的问题,整个扫描过程耗时大概在3分钟左右。
docker run -v "${PWD}:/src" returntocorp/semgrep --config "p/sql-injection" --debug WebGoat/webgoat-lessons -o scan.txt


但是我们知道WebGoat 的sql-injection lessons 应该不止8个漏洞。

在这37个规则中,适合java语言的主要是formatted-sql-string这条规则,因此先单独拉这个规则出来进行分析和优化。
点击对应的名称,可以详细看到规则内容,也可以在线编辑规则、并可运行测试代码来验证规则正确有效性:

更方便一点是在Playground 中对规则进行编写和验证。

完整的formatted-sql-string 注入检测规则:
rules:
- id: formatted-sql-string
languages:
- java
message: |
Detected a formatted string in a SQL statement. This could lead to SQL
injection if variables in the SQL statement are not properly sanitized.
Use a prepared statements (java.sql.PreparedStatement) instead. You
can obtain a PreparedStatement using 'connection.prepareStatement'.
metadata:
asvs:
control_id: 5.3.5 Injection
control_url: https://github.com/OWASP/ASVS/blob/master/4.0/en/0x13-V5-Validation-Sanitization-Encoding.md#v53-output-encoding-and-injection-prevention-requirements
section: "V5: Validation, Sanitization and Encoding Verification Requirements"
version: "4"
category: security
cwe: "CWE-89: Improper Neutralization of Special Elements used in an SQL Command
('SQL Injection')"
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
owasp: "A1: Injection"
references:
- https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- https://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html#create_ps
- https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement
source-rule-url: https://find-sec-bugs.github.io/bugs.htm#SQL_INJECTION
patterns:
- pattern-not: $W.execute(<... "=~/.*TABLE *$/" ...>);
- pattern-not: $W.execute(<... "=~/.*TABLE %s$/" ...>);
- pattern-either:
- pattern: $W.execute($X + $Y, ...);
- pattern: |
String $SQL = $X + $Y;
...
$W.execute($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += $Y;
...
$W.execute($SQL, ...);
- pattern: $W.execute(String.format($X, ...), ...);
- pattern: |
String $SQL = String.format($X, ...);
...
$W.execute($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += String.format(...);
...
$W.execute($SQL, ...);
- pattern: $W.executeQuery($X + $Y, ...);
- pattern: |
String $SQL = $X + $Y;
...
$W.executeQuery($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += $Y;
...
$W.executeQuery($SQL, ...);
- pattern: $W.executeQuery(String.format($X, ...), ...);
- pattern: |
String $SQL = String.format($X, ...);
...
$W.executeQuery($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += String.format(...);
...
$W.executeQuery($SQL, ...);
- pattern: $W.createQuery($X + $Y, ...);
- pattern: |
String $SQL = $X + $Y;
...
$W.createQuery($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += $Y;
...
$W.createQuery($SQL, ...);
- pattern: $W.createQuery(String.format($X, ...), ...);
- pattern: |
String $SQL = String.format($X, ...);
...
$W.createQuery($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += String.format(...);
...
$W.createQuery($SQL, ...);
- pattern: $W.query($X + $Y, ...);
- pattern: |
String $SQL = $X + $Y;
...
$W.query($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += $Y;
...
$W.query($SQL, ...);
- pattern: $W.query(String.format($X, ...), ...);
- pattern: |
String $SQL = String.format($X, ...);
...
$W.query($SQL, ...);
- pattern: |
String $SQL = $X;
...
$SQL += String.format(...);
...
$W.query($SQL, ...);
severity: WARNING
默认规则下的检测结果分析
文章开头提到的使用"sql-injection"默认规则集一共检出了8个sql注入漏洞,经过梳理后发现分别在以下代码文件中:
序号 | 代码文件 | 问题代码行 | 是否误报 | check_id |
1 | SqlInjectionChallenge.java | 63~65 | 否 | java.lang.security.audit.formatted-sql-string.formatted-sql-string |
2 | SqlInjectionLesson10.java | 58~86 | 否 | java.lang.security.audit.formatted-sql-string.formatted-sql-string |
3 | SqlInjectionLesson10.java | 63 | 否 | java.lang.security.audit.sqli.jdbc-sqli.jdbc-sqli |
4 | SqlInjectionLesson2.java | 62 | 否 | java.lang.security.audit.formatted-sql-string.formatted-sql-string |
5 | SqlInjectionLesson8.java | 60~66 | 否 | java.lang.security.audit.formatted-sql-string.formatted-sql-string |
6 | SqlInjectionLesson9.java | 61~86 | 否 | java.lang.security.audit.formatted-sql-string.formatted-sql-string |
7 | SqlInjectionLesson9.java | 66 | 否 | java.lang.security.audit.sqli.jdbc-sqli.jdbc-sqli |
8 | JWTFinalEndpoint.java | 94 | 否 | java.lang.security.audit.formatted-sql-string.formatted-sql-string |
也可以指定输出结果文件格式(例如--json),即 -o result.txt --json (如果-o 后不加任意参数,输出文本格式,很难排查问题,例如规则多的时候不知道匹配到的是哪个规则,出现误报时比较难以排查问题)。

也就是默认输出的格式里,SqlInjectionLesson10.java 匹配到了两条有问题的代码,但是加----线下面的没有标注是匹配到的哪个规则。开始还以为是规则有问题,后来通过--json 文件分析才发现是匹配到了两个不同的check_id(也就是两个规则都各自匹配到了问题代码,一个规则是匹配出第58-85行,另外一个是第63行)

值得注意的是,虽然发现了8个漏洞,但是有两个是重复的(或者说是代码行位置是子集的关系),这是因为分别匹配到了不同的Pattern,也就是表格中加颜色的SqlInjectionLesson10.java和SqlInjectionLesson9.java 。为什么会出现这样,首先来分析下这些Pattern。
以SqlInjectionLesson10.java 为例,单独拿出来看下为什么同一个代码片段会匹配到两次:
首先在 formatted-sql-string 这条规则里面,是下面这条Pattern 起了关键作用。
- pattern: | String $SQL = $X + $Y; ... $W.executeQuery($SQL, ...);
这条规则描述的是一个String 类型的元变量(metavariable)$SQL,可以理解为一个抽象的String 类型值。这个$SQL元变量是由另外两个元变量相加而成,.... 则代表任意值,代表后续可以有代码行也可以没有,最后需要有$W元变量的executeQuery方法,并且第一个参数是上面的$SQL。这是一个比较典型的检测SQL注入漏洞的Pattern,主要的检测逻辑链路是根据executeQuery方法作为有害sql语句的落脚点【Sink】,入参变量是另外两个参数拼接形成的$SQL【Source】。
那么在SqlInjectionLesson10.java 中,可以通过以下代码来抽象整个匹配过程:
$X = SELECT * FROM access_log WHERE action LIKE '%+action $Y = %' $SQL = query $W = statement
2.而在jdbc-sqli 这个规则里面,是- pattern: $S.$METHOD($SQL,...)起了关键作用,$METHOD需要满足什么条件,是通过metavariable-regex 定义了一个正则表达式来限定的,也就是需要方法名符合这个正则^(executeQuery|execute|executeUpdate|executeLargeUpdate|addBatch|nativeSQL)$,因此也就匹配到了SqlInjectionLesson10.java的第63行代码(当前在前面也有pattern-inside:String $SQL = $X + $Y;),以及排除了【String $SQL = "..." + "...";】这种常量相加的方式-不是从用户输入获取的,避免一些误报)。
- metavariable-regex: metavariable: $METHOD regex: ^(executeQuery|execute|executeUpdate|executeLargeUpdate|addBatch|nativeSQL)$
同样地,在SqlInjectionLesson10.java 中,可以通过以下代码来抽象整个匹配过程:
$X = SELECT * FROM access_log WHERE action LIKE '%+action $Y = %' $S = statement $METHOD = executeQuery
完整的jdbc-sqli 规则如下所示,融合了pattern-inside/pattern-not/pattern-either/metavariable-regex ,比较标准和完整,具有一定地参考价值,在自己编写规则地时候可以借鉴。
rules:
- id: jdbc-sqli
languages:
- java
message: |
Detected a formatted string in a SQL statement. This could lead to SQL
injection if variables in the SQL statement are not properly sanitized.
Use a prepared statements (java.sql.PreparedStatement) instead. You
can obtain a PreparedStatement using 'connection.prepareStatement'.
metadata:
category: security
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
patterns:
- pattern-either:
- patterns:
- pattern-either:
- pattern-inside: |
String $SQL = $X + $Y;
...
- pattern-inside: |
String $SQL = String.format(...);
...
- pattern-inside: |
$VAL $FUNC(...,String $SQL,...) {
...
}
- pattern-not-inside: |
String $SQL = "..." + "...";
...
- pattern: $S.$METHOD($SQL,...)
- pattern: |
$S.$METHOD(String.format(...),...);
- pattern: |
$S.$METHOD($X + $Y,...);
- pattern-either:
- pattern-inside: |
java.sql.Statement $S = ...;
...
- pattern-inside: |
$TYPE $FUNC(...,java.sql.Statement $S,...) {
...
}
- pattern-not: |
$S.$METHOD("..." + "...",...);
- metavariable-regex:
metavariable: $METHOD
regex: ^(executeQuery|execute|executeUpdate|executeLargeUpdate|addBatch|nativeSQL)$
severity: WARNING
编写自定义规则
回到文章开头处,我们注意到检测了8个漏洞。但是实际上经过我们分析,其实去掉重复之后就只剩下6个了,并且分布在6个java文件中,熟悉WebGoat的话应该知道这两个规则实际上是漏检了不少sql注入漏洞。
因此整理出了漏检的文件,见下表:
序号 | 文件名 | 问题代码 | 描述 |
1 | SqlInjectionLesson3.java |
| query 是用户输入的参数,直接传入executeUpdate()方法中 |
2 | SqlInjectionLesson4.java |
| query 是用户输入的参数,直接传入executeUpdate()方法中 |
3 | SqlInjectionLesson5.java |
| query 是用户输入的参数,直接传入executeQuery()方法中 |
4 | SqlInjectionLesson5a.java |
| query 是拼接来自用户输入的参数,直接传入executeQuery()方法中 |
5 | SqlInjectionLesson5b.java |
...
...
| accountName 来自用户输入,未用占位符 |
6 | SqlInjectionLesson6a.java |
...中间省略
| query 是拼接来自用户输入的参数,直接传入executeQuery()方法中 |
... | ... | ... | ... |
在WebGoat 的mitigation部分是有一些关键字绕过等等,这部分在编写检测规则时需要花点心思,并且由于缺乏通用性,因此不在此次讨论范围内。
上面漏检的6+个漏洞可以将其分为两类:
第一类:用户输入直接入参Sink函数:序号1,2,3
第二类: 用户输入拼接后入参Sink函数: 序号4,5,6
接下来我们需要根据这两种具体情况编写新的规则。
针对第一类规则:
首先我们需要提炼出这个通用的原型函数,首先是一个变量是从用户输入的(并且参数个数和位置是不固定的),设为$X ,然后这个$X 中间不经过处理函数,最终流向到(executeUpdate|executeQuery)方法中。基于此我们可以增加如下规则:
patterns:
- pattern-inside: |
$VAL $FUNC(...,String $X,...) {
...
}
- pattern: $S.$METHOD($X,...)
- metavariable-regex:
metavariable: $METHOD
regex: ^(executeQuery|executeUpdate)$其中使用pattern-inside用于限定pattern匹配的上下文。因此这里就要限定它来自一个函数,并且形参是String类型的。
扫描了下即发现了4个新的漏洞:

利用这个规则成功地检出了Lesson2、Lesson3、Lesson4、Lesson5 的问题。

针对第二类规则:
和第一类规则类似,还是一个变量是从用户输入的(并且参数个数和位置是不固定的),设为$X ,然后这个$X 经过拼接($X+$Y),最终流向到(executeUpdate|executeQuery)方法中。其中也要考虑多种情况,基于此我们可以增加如下规则:
(1)针对形如 String var = $X:即定义一个变量var,值为$X:尤其适合虽然使用了预编译prepareStatement 但是没有使用?进行占位,依然采取字符串拼接方式。
patterns:
- pattern-inside: |
$VAL $FUNC(...,String $X,...) {
...
String $SQL = "..." + $X;
...
}
- pattern: $S.$METHOD($SQL,...)
- metavariable-regex:
metavariable: $METHOD
regex: ^(executeQuery|executeUpdate|prepareStatement)$(2)针对形如 String var = ""; var = $X; 即先定义一个空值变量,之后重新赋值为$X:
patterns:
- pattern-inside: |
$VAL $FUNC(...,String $X,...) {
...
String $SQL = ...;
...
$SQL = ... + $X + ...;
...
}
- pattern: $S.$METHOD($SQL,...)
- metavariable-regex:
metavariable: $METHOD
regex: ^(executeQuery|executeUpdate|prepareStatement)$将这两种情况规则合并:
- pattern-either:
- patterns:
- pattern-either:
- pattern-inside: |
String $SQL = $X + $Y;
- pattern-inside: |
String $SQL = String.format(...);
...
- pattern-inside: |
$VAL $FUNC(...,String $X,...) {
...
String $SQL = "..." + $X;
...
}
- pattern-inside: |
$VAL $FUNC(...,String $X,...) {
...
String $SQL = ...;
...
$SQL = ... + $X + ...;
...
}
- pattern-not-inside: |
String $SQL = "..." + "...";
...
- pattern: $S.$METHOD($SQL,...)
- metavariable-regex:
metavariable: $METHOD
regex: ^(executeQuery|executeUpdate|prepareStatement)$进行代码扫描测试:

利用这个规则成功地检出了Lesson6a、Lesson5a、Lesson5b 的问题。

写在最后
未来会尝试把Semgrep 接入CI流程中,而精细化策略运营将会成为后续重点研究的方向。鉴于目前笔者能力尚且有限,文章难免会存在错误之处,还望大家不吝赐教。若您对文章有进一步的见解,也欢迎随时与我交流~
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)



