在 Go 库中建模数据流¶
在分析 Go 程序时,CodeQL 不会检查外部包的源代码。若要跟踪不受信任的数据在库中的流动,可以创建库的模型。
可以在 CodeQL 存储库 的 go/ql/lib/semmle/go/frameworks/ 文件夹中找到现有模型。若要添加新模型,应在该文件夹中创建以库命名的新文件。
源¶
若要标记由不受信任的用户控制的数据源,我们将创建一个扩展 RemoteFlowSource::Range 的类。应使用继承和类的特征谓词来精确指定引入数据的流节点。以下是一个来自 Mux.qll 的简短示例。
class RequestVars extends DataFlow::RemoteFlowSource::Range, DataFlow::CallNode {
RequestVars() { this.getTarget().hasQualifiedName("github.com/gorilla/mux", "Vars") }
}
这将使所有对 包 mux 中的函数 Vars 的调用 被视为不受信任的数据源。
流传播¶
默认情况下,我们假定库中的所有函数都没有任何数据流。若要表明某个特定函数确实具有数据流,请创建一个扩展 TaintTracking::FunctionModel(如果不受信任的用户数据在未修改的情况下被传递,则扩展 DataFlow::FunctionModel)的类。
继承和类的特征谓词应指定该函数。该类还应具有签名为 override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp)(或如果扩展 DataFlow::FunctionModel,则签名为 override predicate hasDataFlow(FunctionInput inp, FunctionOutput outp))的成员谓词。主体应约束 inp 和 outp。
FunctionInput 是对函数输入的抽象表示。选项包括
- 接收器 (
inp.isReceiver()) - 参数之一 (
inp.isParameter(i)) - 结果之一 (
inp.isResult(i),如果只有一个结果,则为inp.isResult)
请注意,将函数的结果视为函数输入可能看起来很奇怪,但它在某些情况下是必需的。例如,函数 bufio.NewWriter 返回一个写入器 bw,该写入器缓冲写入操作到基础写入器 w。如果将受污染的数据写入 bw,则有意义的是将该污染传播回基础写入器 w,这可以通过说 bufio.NewWriter 将污染从其结果传播到其第一个参数来建模。
类似地,FunctionOutput 是对函数输出的抽象表示。选项包括
- 接收器 (
outp.isReceiver()) - 参数之一 (
outp.isParameter(i)) - 结果之一 (
outp.isResult(i),如果只有一个结果,则为outp.isResult)
以下是一个来自 Gin.qll 的示例,该示例已略微简化。
private class ParamsGet extends TaintTracking::FunctionModel, Method {
ParamsGet() { this.hasQualifiedName("github.com/gin-gonic/gin", "Params", "Get") }
override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) {
inp.isReceiver() and outp.isResult(0)
}
}
这将使对 gin-gonic/gin 包中接收器类型为 Params 的 Get 方法的调用允许污染从接收器流到第一个结果。换句话说,如果 p 的类型为 Params 并且污染可以流到它,那么在执行完代码行 x := p.Get("foo") 后,污染也可以流到 x。
消毒器¶
不必表明库函数是消毒器。不会分析它们的代码,因此假定数据不会流经它们。
接收器¶
数据流接收器由查询而不是库模型指定。但是,可以使用库模型来表明函数属于哪些特殊类别。然后,查询可以在指定接收器时使用这些类别。表示这些特殊类别的类包含在 CodeQL 存储库 中的 go/ql/lib/semmle/go/Concepts.qll 中。 Concepts.qll 包含用于日志记录机制、HTTP 响应写入器、HTTP 重定向以及编组和反编组函数的类。
以下是一个来自 Stdlib.qll 的简短示例,该示例已略微简化。
private class PrintfCall extends LoggerCall::Range, DataFlow::CallNode {
PrintfCall() { this.getTarget().hasQualifiedName("fmt", ["Print", "Printf", "Println"]) }
override DataFlow::Node getAMessageComponent() { result = this.getAnArgument() }
}
这将使对包 fmt 中的 Print、Printf 或 Println 的任何调用都被识别为日志记录调用。使用日志记录调用作为接收器的任何查询都会识别受污染的数据何时作为参数传递给 Print、Printf 或 Println。