codeql官網(wǎng):https://codeql.github.com/
1. 環(huán)境安裝
根據(jù)官網(wǎng)的提示,先在Visual Studio Code中安裝Codeql擴展??梢詤⒖迹?a target="_blank">https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql
這個Codeql擴展使用Codeql CLI來編譯和運行查詢。從github上下載Codeql CLI。
https://github.com/github/codeql-cli-binaries/releases
Mac系統(tǒng)對應codeql-osx64.zip的版本,下載后解壓到Documents文件夾(Downloads文件夾不被允許),編輯配置文件~/.bash_profile加入如下配置
export PATH=/Users/axisx/Documents/codeql:$PATH
使用source命令生效后,在命令行中輸入codeql,出現(xiàn)如下顯示即安裝成功
axisx@loaclhost Documents % codeql
Usage: codeql <command> <argument>...
Create and query CodeQL databases, or work with the QL language.
PS:之前很多人推薦的在線運行平臺:https://lgtm.com/query。該平臺在2022年12月16日已經(jīng)下線,將LGTM底層的CodeQL分析技術原生集成到GitHub,現(xiàn)在只能用上述方式來運行。
2. codeql 創(chuàng)建數(shù)據(jù)庫
codeql cli能成功運行后,就可以通過相關命令來查詢。相關命令手冊參考官網(wǎng):https://docs.github.com/zh/code-security/codeql-cli/codeql-cli-manual
通過codeql cli來做掃描,主要是兩步,database create創(chuàng)建數(shù)據(jù)庫來存儲程序的層次結構,database analyze運行查詢來分析每個CodeQL數(shù)據(jù)庫,并將結果匯總到SARIF文件中。
(1)創(chuàng)建數(shù)據(jù)庫
查看創(chuàng)建數(shù)據(jù)庫的官方文檔:https://docs.github.com/en/code-security/codeql-cli/getting-started-with-the-codeql-cli/preparing-your-code-for-codeql-analysis
基礎語句為如下。支持的語言包含:C/C++, C#, Go, Java, Kotlin, JavaScript/TypeScript, Python, Ruby, Swift。
codeql database create <database> --language=<language-identifier>
Mac在執(zhí)行codeql命令創(chuàng)建數(shù)據(jù)庫時,出現(xiàn)了一個報錯。Library文件夾下的操作都是operation not permitted。解決方法是進入系統(tǒng)偏好設置>安全和隱私->完全磁盤訪問,勾選“終端”。給終端訪問磁盤的權限。
一般需要指定--source-root,即要掃描的源碼文件夾路徑。否則會把系統(tǒng)上的文件都掃描一遍。另外,如果是為編譯型語言(C/C++, C#, Go, Java, Swift)創(chuàng)建數(shù)據(jù)庫,需要用--command參數(shù)加入編譯命令。以java為例,命令如下。
database create <數(shù)據(jù)庫路徑> --language="java" --command="mvn clean install --file pom.xml" --source-root=<要掃描的項目路徑>
(2)查詢分析數(shù)據(jù)庫
基本語句如下。format是指結果文件的格式,包含CSV、SARIF和Graph格式。
codeql database analyze <database> --format=<format> --output=<output>
codeql database analyze命令主要用于自動化執(zhí)行預定義的安全分析,并生成可用于報告和審查的結果。但也可以手動查詢,查詢語句需要符合QL語言。https://codeql.github.com/docs/ql-language-reference/
使用analyze生成csv,會發(fā)現(xiàn)報告中漏洞位置后面有四個數(shù)字,如"27, 64, 27, 66" ,它們表示代碼中涉及潛在問題的起始行、起始列、結束行和結束列。以下面這個腳本為例,27行64列-27行66列,即url.openConnection();的url
@RequestMapping(value = "/ssrf")
public String One(@RequestParam(value = "url") String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
...
}
(3)源碼導入
在創(chuàng)建數(shù)據(jù)庫時要指定掃描源碼的路徑。如果是github上的項目的話,codeql提供了直接導入url來生成database的功能。點擊DATABASES旁邊的github圖標。然后將repository

3. codeql中的基本元素
既然是要寫查詢語句,就要像了解語句中基本的元素。這些元素都是為了能夠?qū)幊陶Z言更好的解析做的設計。以Java為例介紹一下,其他元素含義參考官網(wǎng):https://codeql.github.com/codeql-standard-libraries/java/index.html
(1) 表達式Expr。表達式簡單理解就是程序中能產(chǎn)生一個值的代碼。它有很多具體劃分,例如邏輯表達式、i++、switch case等等。具體的Expr元素查看:https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Expr.qll/module.Expr.html
(2) 變量Variable。
變量內(nèi)容較少,主要有四種。
LocalScopeVariable 局部變量。LocalVariableDecl像是LocalScopeVariable的一種特例,特指在代碼中顯式聲明的局部變量,如函數(shù)中定義的int a=1。
LocalVariableDecl 局部變量聲明。通常用于表示在函數(shù)、方法或代碼塊內(nèi)部聲明的局部變量。
Parameter 形式參數(shù)。通常用于表示函數(shù)或方法定義中的參數(shù)
Variable是通用的概念,可以表示程序中的任何變量。
(3) 類型Type
類型包含基本類型PrimitiveType、數(shù)組類型Array、引用類型RefType(包含類Class、接口Interface)等。引用類型可以位于頂層 ( TopLevelType) 或嵌套 (NestedType)。類和接口也可以是本地的 ( LocalClassOrInterface, LocalClass) 或匿名的 ( AnonymousClass)。枚舉類型 (EnumType) 和記錄 (Record) 是特殊類型的類。
(4) 類Member
Callable: 代表可調(diào)用實體,通常包括函數(shù)、方法、函數(shù)指針等。a()是構造函數(shù)。A.a()就是可調(diào)用實體
Constructor:構造函數(shù)
Member:類成員的通用抽象,包含方法、構造函數(shù)、字段等
Field: 類或?qū)嵗侄?StaticInitializer: static字段或方法
(5) 聲明Statement
Statement代表程序中的語句。語句通常用于執(zhí)行特定的操作、控制程序的執(zhí)行流程或引入控制結構,例如賦值語句、條件語句、循環(huán)語句等。這些語句中就可能包含Expr表達式來計算值。
Stmt: 所有類型Statement的父類
BlockStmt:
CatchClause: try...catch
ConstCase: switch
ConditionalStmt: if, for, while, dowhile
ForStmt: 循環(huán)
JumpStmt:break, yield, continue
...
Callable庫是方法調(diào)用相關的,Generics庫是泛型相關的。
另外,codeql針對JDK、Struts2、Spring、Android。分別開發(fā)相應的library,更好的解析其中的內(nèi)容。
4. 常用查詢
有了對元素的了解,結合ql語法就可以開始寫查詢語句。以Java為例,介紹一些簡單常用的。參考官方文檔:https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-java/
a. 查詢某種類型的變量,如int類型,示例如下。
import java
from Variable v, PrimitiveType pt
where pt=v.getType() and pt.hasName("int")
select v
b. 查詢泛型接口。如public interface Map<K, V>
import java
from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map") and
pt.getSourceDeclaration() = map
select pt
c. Expr相關查詢
# 查找return為語句的。如果是if語句則是IfStmt
import java
from Expr e
where e.getParent() instanceof ReturnStmt
select e
# 查找方法體
import java
from Stmt s
where s.getParent() instanceof Method
select s
5. 數(shù)據(jù)流分析
數(shù)據(jù)流分析用于計算變量在程序中各個點保存的可能值,確定這些值如何在程序中傳播以及使用它們的位置。
本地的數(shù)據(jù)流分析的元素位于DataFlow模塊。數(shù)據(jù)流可以經(jīng)過的類節(jié)點定義為Node。Node又分為ExprNode和ParameterNode。在數(shù)據(jù)流的基礎上,如果定義某個變量是污點,那么如果從Node From到Node To存在邊,污點跟蹤TaintTracking就成立。
同樣看一下官網(wǎng)的一些案例。官網(wǎng)的數(shù)據(jù)流分析主要分為Local data flow局部數(shù)據(jù)流和Global data flow全局數(shù)據(jù)流。局部數(shù)據(jù)流分析一般指函數(shù)、方法或代碼塊內(nèi)部流動。全局數(shù)據(jù)流則覆蓋了整個代碼庫,可以跟蹤所有變量、函數(shù)調(diào)用之間的數(shù)據(jù)流關系。
Local data flow
a. 查找傳入new FileReader(..)中的文件名
import java
import semmle.code.java.dataflow.DataFlow
from Constructor fileReader, Call call, Expr src
where
fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
call.getCallee() = fileReader and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
# 如果要使源更加具體可以將DataFlow::exprNode(src)換為DataFlow::parameterNode(p)
select src
Call屬于Expr類,可以對方法構造函數(shù)等進行調(diào)用。其getCallee()方法是獲取可調(diào)用的目標。
b. 查找對格式字符串未硬編碼的格式化函數(shù)的調(diào)用。
格式化代碼一般為String.format("I am %d years old.", age);
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.StringFormat
from StringFormatMethod format, MethodAccess call, Expr formatString
where
call.getMethod() = format and
call.getArgument(format.getFormatStringIndex()) = formatString and
not exists(DataFlow::Node source, DataFlow::Node sink |
DataFlow::localFlow(source, sink) and
source.asExpr() instanceof StringLiteral and
sink.asExpr() = formatString
)
select call, "Argument to String format method isn't hard-coded."
現(xiàn)在的版本MethodAccess是MethodCall,也就是找到調(diào)用方法為String.format()方法。format.getFormatStringIndex()是一個用于獲取格式化字符串參數(shù)的方法,返回的是格式化字符串在參數(shù)列表中的索引值。然后獲取這個索引值的參數(shù)。not exists表示不存在數(shù)據(jù)流路徑,也就是String.format中的值不是傳入的,這樣就不存在數(shù)據(jù)流。asExpr()將節(jié)點source和sink都轉(zhuǎn)換成表達式節(jié)點。判斷source為字符串常量,sink為格式化字符串函數(shù)。
c.查找所有硬編碼字符串java.net.URL
import semmle.code.java.dataflow.DataFlow
from Constructor url, Call call, StringLiteral src
where
url.getDeclaringType().hasQualifiedName("java.net", "URL") and
call.getCallee() = url and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src
這個有了上面的分析理解起來就很簡單了。重點在于StringLiteral,它代表字符串或text block。數(shù)據(jù)流的源Node如果是字符串(非變量),傳入到URL的第一個參數(shù)中,那么URL就是硬編碼的。
Global data flow
局部數(shù)據(jù)流是DataFlow::localFlow,全局數(shù)據(jù)流是DataFlow::Global<ConfigSig>。全局數(shù)據(jù)流包含四個重點謂詞
isSource :定義數(shù)據(jù)從哪兒流出
isSink:定義數(shù)據(jù)流向哪兒
isBarrier: 限制數(shù)據(jù)流(可選項)
isAdditionalFlowStep: 添加額外的流程步驟 (可選項)
全局數(shù)據(jù)流分析的基本格式如下
import semmle.code.java.dataflow.DataFlow
module MyFlowConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { ... }
predicate isSink(DataFlow::Node sink) { ... }
}
module MyFlow = DataFlow::Global<MyFlowConfiguration>;
from DataFlow::Node source, DataFlow::Node sink
where MyFlow::flow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
全局污點跟蹤針對全局數(shù)據(jù)流,所以基本格式與上述全局數(shù)據(jù)流分析格式相似。只需要把DataFlow換成TaintTracking。
官方給的一些Global data flow的案例:
a. 使用全局數(shù)據(jù)流編寫一個查詢,查找所有用硬編碼字符串創(chuàng)建java.net.URL的。
import semmle.code.java.dataflow.DataFlow
module LiteralToURLConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source.asExpr() instanceof StringLiteral
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
sink.asExpr() = call.getArgument(0) and
call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
)
}
}
module LiteralToURLFlow = DataFlow::Global<LiteralToURLConfig>;
from DataFlow::Node src, DataFlow::Node sink
where LiteralToURLFlow::flow(src, sink)
select src, "This string constructs a URL $@.", sink, "here"
和局部數(shù)據(jù)流分析很類似,只需要用全局數(shù)據(jù)流的格式寫即可。只不過局部變量用from先把用到的變量類型聲明了一遍,但是在全局數(shù)據(jù)流分析中在exists函數(shù)中聲明的變量類型。
b. 編寫一個類來表示從java.lang.System.getenv傳遞的數(shù)據(jù)流。該方法的示例代碼如:String javaHome = System.getenv("JAVA_HOME");
import java
class GetenvSource extends MethodAccess {
GetenvSource() {
exists(Method m | m = this.getMethod() |
m.hasName("getenv") and
m.getDeclaringType() instanceof TypeSystem
)
}
MethodAccess在現(xiàn)在的版本里已經(jīng)改為MethodCall。有關方法的操作都位于Method中,而在數(shù)據(jù)流中對應的是MethodCall。首先獲取數(shù)據(jù)流MethodCall中對應的方法,判斷這個方法名是否為getenv,判斷聲明這個方法的類型是否為java.lang.System。由于Codeql中集成了JDK的庫。在JDK.qll中有如下的代碼。所以只需要判斷類型是否為TypeSystem。
class TypeSystem extends Class {
TypeSystem() { this.hasQualifiedName("java.lang", "System") }
}
結合a和b的案例,就可以寫一個全局數(shù)據(jù)流分析,從getenv到java.net.url
c. 編寫一個查詢來查找未被任何其他方法調(diào)用的方法
import java
from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and # 是否為源文件
not callee.hasName("<clinit>") and not callee.hasName("finalize") and # 這兩個是隱式調(diào)用的可以排除
not callee.isPublic() and
not callee.(Constructor).getNumberOfParameters() = 0 and
not callee.getDeclaringType() instanceof TestClass
select callee, "Not called."
代碼庫中的方法很多都不會被調(diào)用,所以在查詢時應該將庫中的方法排除。也就是檢查是否是源文件。