0.简介
利用codeql官方qll中的类(例如TaintTracking::Configuration)进行污点分析时,如果只定义source点和sink点,则可能产生一些误报和漏报。此时就需要使用isSanitizer和isAdditionalTaintStep来消除这些误报和漏报:
isSanitizer:例如数据的传播路径中,可能进入了某个函数是专门用来过滤Sql注入的,但codeql并不能自动的识别这些函数会导致污点数据被过滤,从而产生误报,此时就需要我们自己定义isSanitizer谓词来帮助CodeQL识别过滤函数减少误报。isAdditionalTaintStep:而对于某个第三方包中的函数调用,CodeQL也自然无法知道该函数调用是否会导致污点数据被过滤,因此CodeQL默认认为会被过滤,这样又会产生许多漏报,此时就需要我们自己定义isAdditionalTaintStep谓词来告诉CodeQL哪些调用还是污点传播。
1.isAdditionalTaintStep
要使用这个谓词,首先需要知道到底哪些情况会导致污点数据的传播断掉。根据一个github上的一个issues 《Can someone explain what isAdditionalTaintStep means?》 可以看到。

官方对这个isAdditionalTaintStep的作用解释是这样的:CodeQL进行代码分析时,只会分析用户的代码,对于第三方包的方法调用被将认为是不可预测的,因此就需要自己定义isAdditionalTaintStep。以下自己定义一个jar包来测试一下。首先定义jar包代码如下:
package test.jus4fun.xyz;
public class Test <T>{
public T test(T input){
return input;
}
}
只有一个Test类,该类仅定义一个test方法,这个方法的作用就是输入什么就返回什么。新建一个Springboot项目,假如需要分析的代码如下:
package com.example.xssdemo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import test.jus4fun.xyz.Test;
@RestController
@RequestMapping("/")
public class XSSDemo {
@RequestMapping("/xss")
public String XSS(@RequestParam(value = "input") String input){
return input;
}
@RequestMapping("/xss2")
public String XSS2(@RequestParam(value = "input") String input){
String newInput;
Test test = new Test();
newInput = (String) test.test(input);
return input;
}
}
很显然XSS1和XSS2方法都存在反射型XSS漏洞。使用如下ql进行查询。
/**
* @kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class XSSConfig extends TaintTracking::Configuration {
XSSConfig() { this = "XSSConfig" }
override predicate isSource(DataFlow::Node node) {
node instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node node) {
node.asExpr().getEnclosingCallable() instanceof SpringRequestMappingMethod and
node.asExpr().getEnclosingStmt() instanceof ReturnStmt
}
}
from XSSConfig config,DataFlow::PathNode source,DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(),source,sink,"test"
查询的结果确实只发现了第一处XSS。

这里原因就是XSS2方法中使用了一个外部jar包里的Test#test方法,导致数据流从@RequestParam(value = "input") String input到newInput = (String) test.test(input);断开了。

此时就可以定义一个isAdditionalTaintStep将这两个node点连接起来。这里在XSSConfig里添加一个isAdditionalTaintStep,代码如下:
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(Call call |
node1.asExpr().getType().hasName("String") and
node1.asExpr().getEnclosingCallable().hasName("XSS2") and
node2.asExpr()=call and
call.getCallee().hasName("test")
)
}
重新扫描可以看到确实可以扫出来第二个XSS了。

这个写的时候要注意,isAdditionalTaintStep可以将两个完全不相干的点连接起来,之前看别人的写的文章里面有一些定义的isAdditionalTaintStep是错误的,导致扫描出现非常奇怪的数据流。例如这里我们可以定义一个isAdditionalTaintStep将XSS方法中的return input和XSS2方法中的newInput = (String) test.test(input);连接起来,代码如下:
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(Call call |
node1.asExpr().getType().hasName("String") and
node2.asExpr() = call and
call.getCallee().hasName("test")
)
}

可以看到,如果你强行定义一个非常离谱的isAdditionalTaintStep,就可能导致这种离谱的扫描结果出现。如果你想默认将所有函数调用都当成会传播污点的,可以定义isAdditionalTaintStep如下:
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(Call call |
node2.asExpr()=call and
call.getAnArgument()=node1.asExpr()
)
}
除了上面这种函数调用污点传递到返回值的的情况,还有一些其他的典型写法。例如,如果在外部包中声明一个类TestTaintStep,代码如下:
package test.jus4fun.xyz;
public class TestTaintStep {
String name;
public void setName(String input){
this.name = input;
}
public String getName(){
return this.name;
}
}
在Springboot项目中新建一个XSS4方法,代码如下:
@RequestMapping("/xss4")
public String XSS4(@RequestParam(value = "input") String input){
String newInput,safeInput;
TestTaintStep test = new TestTaintStep();
test.setName(input);
String outPut = test.getName();
return outPut;
}
此时使用刚才的规则将无法扫描出这一条污点链路,主要原因还是因为外部方法调用导致。这里两个导致污点传播断开的节点为test.setName(input)和test.getName()。上文中定义的isAdditionalTaintStep中,表示的情况是污点从参数input传播给Call test,而赋值语句默认传播污点,因此test传播给了newInput。

而这里首先第一个部分test.setName(input)中,污点实际从input参数通过setName方法传播到了test这个实例。第二处污点String outPut = test.getName();是从实例test,传播到了getName这个Call中,然后从getName传播到了outPut变量。因此可以额外添加这两种特殊情况的step:
//针对第一种情况即参数通过call传播到实例
exists(Call call |
node1.asExpr() = call.getAnArgument() and
node2.asExpr() = call.getQualifier()
)
//针对第二种情况即从实例传播到call
exists(Call call |
node1.asExpr()= call.getQualifier() and
node2.asExpr() = call
)
总的来说主要就是三种情况,即外部包的调用中:
- 污点从参数传播到函数调用的返回结果中
- 污点从参数通过Call传播到实例中
- 污点从实例传播到Call的返回结果中
2.isSanitizer
Sanitizer,意为消毒剂。顾名思义这个谓词就是判断哪里可能会使污点数据变得清清白白。比如,有如下代码:
@RequestMapping("/xss3")
public String XSS3(@RequestParam(value = "input") String input){
String newInput,safeInput;
Test test = new Test();
newInput = (String) test.test(input);
safeInput = xssEncode(newInput);
return safeInput;
}
private static String xssEncode(String s){
if (s == null || s.isEmpty()) {
return s;
}
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c!='>' && c!='<') {sb.append(c);}
}
return sb.toString();
}
使用之前的ql查询这个xss3也会被判断存在XSS。

实际上xssEncode方法已经对污点数据进行了过滤,使污点数据变成了清清白白的数据。因此就可以定义一个isSanitizer,代码如下:
override predicate isSanitizer(DataFlow::Node node) {
exists(Call call|
node.asExpr() = call and
call.getCallee().hasName("xssEncode")
)
//认为在某个节点调用了xssEncode及污点数据被洗白
}
此时将不会再产生这条误报。
参考
- https://codeql.github.com/docs/
- 同事小钾的亲身教学