在前面的文章中,我们首先为读者展示了如何通过LGTM查询控制台来编写和运行分析JavaScript和TypeScript代码的查询。然后,讲解了CodeQL的JavaScript标准库中用于从文本级别和词法级别分析源代码的常用类及其谓词。在本文中,我们继续讲解CodeQL的JavaScript标准库中的其他类和谓词。
声明与绑定模式
我们知道,变量都是通过声明语句(类DeclStmt)进行声明的,而声明语句主要有三种:var语句(由类VarDeclStmt表示),const语句(由类ConstDeclStmt表示)和let语句(由类LetStmt表示)。并且,每个声明语句都含有一个或多个声明符,具体由类VariableDeclarator表示。
每个声明符由谓词VariableDeclarator.getBindingPattern()返回的绑定模式和VariableDeclarator.getInit()返回的可选初始化表达式组成。
通常情况下,绑定模式就是一个简单的标识符,如var x=42。然而,在ECMAScript2015及其更高的版本中,它也可以是更复杂的模式,如var [x, y] = arr。
实际上,各种绑定模式都是由类BindingPattern及其子类表示的:
· varref:L值位置的简单标识符,例如var x中的x,或x=42中的x
· Parameter:函数或catch子句的参数
· ArrayPattern:数组模式,例如,[x,y]=arr语句中的左边部分
· ObjectPattern:对象模式,例如{x, y: z} = o语句中的左边部分
下面是一个查询示例,它用于查找多次声明同一个变量的声明语句:
import javascript from DeclStmt ds, VariableDeclarator d1, VariableDeclarator d2, Variable v, int i, int j where d1 = ds.getDecl(i) and d2 = ds.getDecl(j) and i < j and v = d1.getBindingPattern().getAVariable() and v = d2.getBindingPattern().getAVariable() and not ds.getTopLevel().isMinified() select ds, "Variable " + v.getName() + " is declared both $@ and $@.", d1, "here", d2, "here"
下面是上面的代码的运行结果:
由于这个问题很少见,因此,我们只在lgtm.com上的angular/angular.js项目找到了一个含有该问题的实例。
需要注意的是,这里及后面几个查询中都使用了not ... isMinified()。其作用是排除了在压缩型代码中找到的结果。如果我们删除and not ds.getTopLevel().isMinified(),并重新运行该查询,则会报告Meteor/Meteor项目中的压缩型代码中找到的两个结果。
属性
对象字面量中的属性是由类Property表示的,它也是ASTNode的子类,但既不是Expr的子类,也不是STMT的子类。
类Property有两个子类,即ValueProperty和PropertyAccessor类,它们分别表示常规值属性和getter/setter属性。同时,类PropertyAccessor又有两个子类:PropertyGetter和PropertySetter类,它们分别表示getter和setter。
谓词Property.getName()和Property.getInit()用于访问已定义的属性的名称及其初始值。对于PropertyAccessor及其子类来说,可以覆写getInit()使其返回getter/setter函数。
下面,我们将通过一个例子来介绍如何查询属性。在这里,该查询用于查找包含两个同名属性的对象表达式,但不包括压缩型代码中的结果:
import javascript from ObjectExpr oe, Property p1, Property p2, int i, int j where p1 = oe.getProperty(i) and p2 = oe.getProperty(j) and i < j and p1.getName() = p2.getName() and not oe.getTopLevel().isMinified() select oe, "Property " + p1.getName() + " is defined both $@ and $@.", p1, "here", p2, "here"
上述示例的运行结果如下图所示:
我们看到,许多项目都含有带有两个同名属性的对象表达式。
模块
分析JavaScript代码的标准库支持使用ECMAScript2015模块,以及遗留的CommonJS模块和AMD风格的各种模块。其中,类ES2015Module、NodeModule和AMDModule分别用于表示上面所说的这三种类型的模块,并且这它们都继承自公共超类Module。
与模块定义相关的重要成员谓词包括:
· Module.getName():获取模块的名称,注意,它获取的名称只是模块名称的主干部分,而不包括扩展名。
· Module.getAnImportedModule():获取本模块(通过import或require)导入的其他模块。
· Module.getAnExportedSymbol():获取本模块导出的符号的名称。
此外,分析JavaScript代码的标准库还提供了一个Import类,用于处理ECMAScript2015风格的import语句以及CommonJS/AMD风格的require语句;此外,通过该类的成员谓词Import.getImportedModule,还可以访问导入的模块,如果可以静态地确定该模块的话。
名称绑定
为了对名称绑定进行建模,用于处理JavaScript代码的标准库使用了4个概念:作用域、变量、变量声明和变量访问,分别由类Scope、Variable、VarDecl和VarAccess表示。
作用域
在ECMAScript 5中,共有三种作用域:全局作用域(每个程序一个)、函数作用域(每个函数一个)和catch子句作用域(每个catch子句一个)。这三种作用域分别由类GlobalScope、FunctionScope和CatchScope作用域表示。ECMAScript 2015为let-bound变量增加了块作用域,这些作用域也是通过类作用域(Scope)、类表达式作用域(ClassExprScope)和模块作用域(ModuleScope)表示的。
用于表示类作用域的Scope类提供了以下应用编程接口:
· Scope.getScopeElement() 返回指向该作用域的AST节点;对于GlobalScope,尚未定义。
· Scope.getOuterScope()返回该作用域的词法封闭作用域。
· Scope.getAnInnerScope()返回一个词法上嵌套在此作用域内的作用域。
· Scope.getVariable(name)、Scope.getAVariable()返回在该作用域内(隐式或显式)声明的变量。
变量
Variable类可用于对JavaScript程序中的所有变量进行建模,包括全局变量、局部变量和参数(函数和catch子句),无论它们是否显式声明的。
重要的是,不要把变量与其声明搞混了:局部变量可能有多个声明,而全局变量和隐式声明的局部参数变量根本不需要进行声明。
变量的声明和访问
变量可以通过变量声明符、函数声明语句和表达式、类声明语句或表达式、或者函数和catch子句的参数进行声明。虽然这些声明的语法形式有所不同,但在所有情况下,都有一个标识符来命名声明的变量。我们可以认为该标识符就是正确的声明,并将其赋给类VarDecl。 另一方面,引用变量的标识符被赋给类VarAccess。
与变量、变量的声明及访问相关的重要谓词包括:
· Variable.getName()、VarDecl.getName()、VarAccess.getName()用于返回变量的名称。
· Variable.getScope()用于返回变量所属的作用域。
· Variable.isGlobal()、Variable.isLocal()、Variable.isParameter() 分别用于确定变量是否为全局变量、局部变量或者参数变量。
· Variable.getAnAccess() 用于将一个变量映射到引用它的所有VarAccesses。
· Variable.getADeclaration()用于将一个变量映射到声明它的所有VarDecls(可能不存在,也可能有一个或多个)。
· Variable.isCaptured() 确定变量是否在一个词法上嵌套在声明变量的作用域内的作用域中被访问。
例如,考虑下面的查询,该查询用于查询声明相同变量的不同函数声明,即在同一作用域内的两个互相冲突的函数声明(同样不包括压缩型代码):
import javascript from FunctionDeclStmt f, FunctionDeclStmt g where f != g and f.getVariable() = g.getVariable() and not f.getTopLevel().isMinified() and not g.getTopLevel().isMinified() select f, g
上述查询代码的运行结果如下所示:
我们可以看到,有些项目声明同名的函数,并依赖于平台特定的行为来消除这两个声明的歧义。
控制流
库CFG.qll为我们提供了许多类,利用这些类,我们可以从过程内控制流图(CFG)的角度来描述程序。
在这些类中,类ControlFlowNode用于表示控制流图中的单个节点,当然,它可以是表达式、语句或合成控制流节点。需要注意的是,类Expr和Stmt并非继承自CodeQL级别的ControlFlowNode类,尽管它们的实体类型是兼容的;因此,如果需要在基于AST的程序表示形式和基于CFG的程序表示形式之间进行映射的话,可以显式地从一种形式转换为另一种形式。
合成控制流节点有两种:入口节点(类ControlFlowEntryNode)和出口节点(类ControlFlowExitNode),前者表示顶层模块或函数的开始,后者表示它们的结束。注意,它们不对应于任何AST节点,而只是用于控制流图的唯一入口点和出口点。此外,我们还可以通过谓词StmtContainer.getEntry()和StmtContainer.getExit()来访问入口和出口节点。
大多数(但并非所有)顶层模块和函数都有另外一个CFG节点,即起始节点。这是开始执行的CFG节点。与作为合成构造的入口节点不同,起始节点对应于实际的程序元素:对于顶层模块来说,它是第一条语句的第一个CFG节点;但是对于函数来说,它是对应于它们的第一个参数的CFG节点,或者,如果没有参数的话,则是函数主体的第一个CFG节点。当然,空的顶层模块没有起始节点。
在大多数情况下,使用起始节点比使用入口节点更可取。
控制流图的结构反映在ControlFlowNode的成员谓词中:
· ControlFlowNode.getASuccessor()返回一个ControlFlowNode,即控制流图中当前ControlFlowNode的后继节点。
· ControlFlowNode.getAPredecessor()的作用是返回控制流图中当前ControlFlowNode前面的节点。
· ControlFlowNode.isBranch()确定该节点是否有多个后继节点。
· ControlFlowNode.isJoin()确定该节点前面是否还有多个节点。
· ControlFlowNode.isStart()确定该节点是否为起始节点。
许多基于控制流的分析都是根据基本块而不是单个控制流节点进行的,其中,基本块就是没有分支或联接的控制流节点的最大序列。此外,库BasicBlocks.qll提供的类BasicBlock可以用于表示所有类型的基本块。与ControlFlowNode类似,它也提供成员谓词getASuccessor()和getAPredecessor(),用于在基本块级别上导航控制流图;此外,它还提供成员谓词getANode()、getNode(int)、getFirstNode()和getLastNode(),用于访问基本块内的各个控制流节点。另外,谓词Function.getEntryBB()可以返回函数中的入口基本块,即包含函数的入口节点的基本块。类似地,Function.getStartBB()提供了对起始基本块的访问方法,其中包含函数的起始节点。对于CFG节点,我们可以优先选用getStartBB(),而非getEntryBB()。
下面,我们将举例说明如何利用基本块进行分析。在本例中,BasicBlock.isLiveAtEntry(v, u) 用于确定变量v在给定基本块的入口处是否有效;如果有效,则将u绑定到引用其在入口处的值的变量v的使用上。通过它,我们可以查找函数中特定的全局变量的使用情况——每次进行读操作之前都会执行一次写操作的全局变量——这表明该变量本应声明为局部变量的:
import javascript from Function f, GlobalVariable gv where gv.getAnAccess().getEnclosingFunction() = f and not f.getStartBB().isLiveAtEntry(gv, _) select f, "This function uses " + gv + " like a local variable."
上述代码的运行结果如下所示:
我们看到,许多项目都含有这种类型的变量,它们看起来好像本应定义为局部变量的。
数据流
定义和使用
库DefUse.qll为我们提供了许多类和谓词,可用于确定变量的定义和使用之间的def-use关系。
其中,类VarDef和VarUse分别提供了与变量的定义和使用相关的所有表达式。对于前者,我们可以通过谓词VarDef.getAVariable()来找出哪些变量是由给定的变量定义来定义的。类似地,谓词VarUse.getVariable()可用于返回某次使用所访问的(单个)变量。
此外,def-use信息本身是由谓词VarUse.getADef()提供的,该谓词可以从变量的使用之处连接到其定义之处,而其定义之处也可以到达其使用之处。
例如,下面的查询用于查找从未使用过的局部变量的定义;也就是说,变量要么在定义之后根本没有被引用,要么它的值被覆盖了:
import javascript from VarDef def, LocalVariable v where v = def.getAVariable() and not exists (VarUse use | def = use.getADef()) select def, "Dead store of local variable."
上面代码的运行结果如下所示:
我们看到,许多项目中都存在一些赋值后从未用过的局部变量。
SSA
库semmle.javascript.SSA提供了一种更加细致的基于静态单赋值形式(SSA)的程序数据流表示方法。
在SSA形式中,每次使用局部变量都会为其提供一个(SSA)定义。并且,SSA定义由类SsaDefinition来表示。实际上,它们并非AST节点,因为并不是每个SSA定义都对应于源代码中的显式元素。
实际上,SSA定义共有五种形式:
· 显式定义(SsaExplicitDefinition):这些定义会直接封装一个VarDef类,即像x=1这样显式出现在源代码中的定义。
· 隐式初始化(SsaImplicitInit):它们表示在作用域开始处带有未定义的局部变量的隐式初始化。
· Phi节点(SsaPhiNode):这些是伪定义,在必要时合并两个或多个SSA定义。
· 变量捕获(SsaVariableCapture):这些伪定义出现在代码中的某些地方,在这些地方,捕获的变量的值可能会在没有显式赋值的情况下被改变,例如,由于函数调用而改变,等等。
· 细化节点(SsaRefinementNode):这些也是伪定义,它们出现在代码中对变量已经有所了解的位置;例如,一个条件语句if(x===null)将导致在其“then”分支的开始处产生一个细化节点,该节点记录了已知变量x在那里为null的事实。(在文献中,这些有时称为"pi节点"。)
数据流节点
实际上,semmle.javascript.dataflow.DataFlow库不仅可以用来表示变量的定义和使用,还能以数据流图的形式来刻画程序,其中的节点就是DataFlow::Node类的值,该类有两个子类:ValueNode和SsaDefinitionNode。其中,前者的节点用于封装生成值的表达式或语句(具体地说,函数或类声明语句,或TypeScript命名空间或枚举声明);后者的节点用于封装SSA定义。
我们既可以使用谓词DataFlow::valueNode将表达式、函数或类转换为其相应的ValueNode,也可以使用DataFlow::ssaDefinitionNode将SSA定义映射为其相应的SsaDefinitionNode。
此外,该库还提供一个辅助谓词DataFlow::parameterNode,可以将一个参数映射到其对应的数据流节点。(实际上,它只是对DataFlow::ssaDefinitionNode进行了封装,因为其参数也被认为是SSA定义。)
另一方面,该库还提供了一个谓词ValueNode.getAstNode(),用于将ValueNodes映射到ASTNodes;以及一个谓词SsaDefinitionNode.getSsaVariable(),用于将SsaDefinitionNodes映射到SsaVariables。此外,该库还提供了一个实用工具谓词Node.asExpr(),用于获取ValueNode的底层表达式,并且对于所有不对应于表达式的节点来说,都被认为是未定义的。(特别需要注意的是,该谓词并不是为ValueNodes封装函数或类声明语句而定义的!)
我们可以使用谓词DataFlow::Node.getAPredecessor()来查找其值可能流入本节点的其他数据流节点,而谓词getASuccessor的查找方向则正好相反。
例如,下面的查询可以查找针对名为send的方法的所有调用,并且调用的值来自名为res的参数,这表明它可能正在发送一个HTTP响应:
import javascript from SimpleParameter res, DataFlow::Node resNode, MethodCallExpr send where res.getName() = "res" and resNode = DataFlow::parameterNode(res) and resNode.getASuccessor+() = DataFlow::valueNode(send.getReceiver()) and send.getMethodName() = "send" select send
上面的代码的运行结果如下所示:
如上图所示,我们在AMP HTML项目中找到了发送的HTTP响应。
注意,这个库只能对过程内部的数据流进行建模,也就是说,无法针对跨函数调用和函数的返回进行建模。同样,也无法对对象属性和全局变量的流动进行建模。
类型推断
库semmle.javascript.dataflow.TypeInference为JavaScript实现了一种简单类型推断方法,它是基于过程内的、对堆不敏感的数据流分析而实现的。基本上,推断算法将变量和表达式运行过程中具体的可能值近似表示为抽象值的集合(由类AbstractValue表示),其中每个抽象值代表一组具体值。
例如,假设有一个抽象值表示所有非零数字,另一个抽象值表示除可转换为数字的字符串外的所有非空字符串。那么,这两个抽象值都是一种近似的表示方法,表示是一个非常大的具体值集合。
而其他抽象值则更加精确,它们可以精确到单个具体值。例如,我们可以用一个抽象值表示具体的null值,用另一个抽象值表示数字0。
此外,还有一组特殊的抽象值,称为不确定的抽象值,用于表示所有的具体值。我们可以通过它们来处理无法推断出更精确的值的表达式,例如函数参数(如上所述,该分析是过程内的,因此,无法对参数传递进行建模)或属性的值(该分析也不对属性值进行建模)。
每个不确定的抽象值都有一个字符串值,用于描述不确定性的来源。在上面的例子中,参数值的不确定性来自“call”,而属性值的不确定性来自“heap”。
要检查抽象值是否是不确定的,可以使用isIndefinite成员谓词。它的单一参数用于描述不确定性的来源。
每个抽象值都有一个或多个关联类型(CodeQL的InferredType类大致对应于由typeof运算符处理的类型标记,例如null、undefined、boolean、number、string、function、class、date和object。
为了访问类型推断的结果,可以使用类DataFlow::AnalyzedNode:任何DataFlow::Node都可以强制转换为该类,此外,还有一个用起来非常方便的谓词Expr::analyze,可用于将表达式直接映射到相应的AnalyzedNode。
一旦获得了AnalyzedNode,就可以使用谓词AnalyzedNode.getAValue()访问为其推断的抽象值,并使用getAType()来获取推断出的类型。
例如,下面的查询,用于检查不能为null的表达式是否为null:
import javascript from StrictEqualityTest eq, DataFlow::AnalyzedNode nd, NullLiteral null where eq.hasOperands(nd.asExpr(), null) and not nd.getAValue().isIndefinite(_) and not nd.getAValue() instanceof AbstractNull select eq, "Spurious null check."
换句话说,上面的代码实际上就是在查找这样的相等测试eq:其中一个操作数是字面值null,另一个操作数是转换为AnalyzedNode的某个表达式。如果该节点的类型推断结果是精确的(也就是说,推断的值中没有一个是不确定的),并且null(的抽象表示)不在其中,那么,它就是我们要找的eq。
当然,我们也可以添加自定义类型推理规则,为此,可以通过继承DataFlow::AnalyzedNode来定义新的子类,并覆写getAValue。同时,我们还可以通过扩展抽象类CustomAbstractValueTag(它是string的子类)来引入新的抽象值:属于该类的每个字符串都引入与CustomAbstractValue类型相应抽象值。此外,我们可以使用谓词CustomAbstractValue.getTag()将抽象值映射为其相应的标记。通过实现CustomAbstractValueTag类的抽象谓词,我们可以定义自定义抽象值的语义,例如原来的哪些值必须进行类型转换,以及转换成什么样的类型。
小结
前面的文章介绍了用于从文本级别和词法级别分析JavaScript源代码的常用类及其谓词,在本文中,我们为读者讲解了用于从句法层次分析JavaScript源代码的类和谓词。
备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。
参考资料:https://help.semmle.com/
如若转载,请注明原文地址