代码分析平台CodeQL学习手记(十五)
导语:从本文开始,我们为读者介绍如何利用CodeQL的标准库来分析JavaScript和TypeScript代码中潜在的安全漏洞。
在前文中,我们为读者详细介绍了如何在命令行环境下构建CodeQL数据库、更新数据库,以及如何通过自定义查询来分析数据库。从本文开始,我们为读者介绍如何利用CodeQL的标准库来分析JavaScript和TypeScript代码中潜在的安全漏洞。
首先,让我们了解一下如何通过LGTM查询控制台来编写和运行用于分析JavaScript和TypeScript代码的查询。
利用查询控制台编写并运行JavaScript查询代码
在JavaScript语言中,所有的表达式都是使用表达式语句来表示的。这样做的好处是用起来非常方便,但是也可能会带来一些问题。例如,假设程序员想通过赋值语句x = 42给变量x赋一个新值。 但是,由于粗心,键入了两个等号,这时,它就变成了一个比较语句x == 42。 由于这个语句在语法上是正确的,所以,编译和执行时并不会报错:它只是将x与42进行比较,然后,将比较的结果丢弃。好了,我们现在的任务,就是编写一个查询,专门寻找此类语句。
在编写查询之前,让我们先来看看如何在查询控制台中运行查询。
运行查询代码
首先,打开查询控制台,地址为https://lgtm.com/query。然后,从Language下拉列表中选择JavaScript,并从Project下拉列表中选择要查询的一个或多个项目。接着,将以下查询代码复制到查询控制台的文本框中:
import javascript from Expr e where e.isPure() and e.getParent() instanceof ExprStmt select e, "This expression has no effect."
这时,LGTM会自动编译查询代码,并检查是否存在语法错误;如果一切正常的话,“Run”按钮将变成绿色的,表示查询代码已经通过了编译,可以运行了,具体如下图所示:
单击“Run”按钮,查询就会开始运行,运行结束后,原来的按钮会变成一个紫色的“View results”按钮。点击这个按钮,我们就可以看到查询代码返回的结果了:
如您所见,结果通常展示在项目名称下面。这里的查询结果分为两列,分别对应于查询的select子句中的两个表达式。第一列对应于表达式e,并链接到项目源代码中e所在的位置。第二列是警报消息。结果底部的省略号(…)表示有更多的结果可用,单击它就会显示更多的结果。
如果返回的结果数量不为0,单击e列中的相关链接,就会在代码查看器中看到符合要求的表达式。
解读查询代码
现在,我们来解释一下这个查询中的各个语句。首先,开头部分是一个import语句,其作用是导入相应的标准库。在此之后,这个查询还包含三个语句,它们的作用与 SQL查询中的FROM、WHERE和SELECT子句的作用基本相同。
下面,我们进行逐行解读:
import javascript
这一行代码的作用是,导入用于分析javascript代码的标准查询库。实际上,每个查询的开头部分都会有一个或多个import语句。
from Expr e
这个from子句的作用是定义了一个变量,该变量的名称为e,其类型为 Expr,表示javascript语言中的表达式。下面给出定义变量的一般形式为:
现在,我们来看下一个语句:
where e.isPure() and e.getParent() instanceof ExprStmt
该语句定义了变量e需要满足的条件。其中,e.isPure()表示这是一个“纯粹的”表达式,纯粹到啥用也没有。而e.getParent() instanceof ExprStmt则表示该表达式的父类为表达式语句。那么,中间位置的and是什么作用呢?其实,and是一个逻辑连接词,表示前后的判断必须同时成立,则整个判断才能成立。
select e, "This expression has no effect."
这里的select子句的作用是,指出要显示哪些内容。就本例来说,就是显示符合要求的表达式,并给出一个解释性的文本,也就是上面的字符串。一般来说,用于查找不良编码实践实例的查询的select语句总是采用以下形式:
select
改进查询代码
通常来说,查询代码的编写过程就是一个迭代的过程。例如,刚开始的时候,我们编写了一个简单的查询,通过运行,可能会发现以前没有考虑过的情况,或需要改进的地方,然后,着手进行修改,再次运行,如果有必要,继续进行修改,依此类推。
消除假阳性结果
如果仔细浏览上面查询返回的结果,就会发现我们的代码还有待改进,因为返回的结果中,有一些是类似use strict这样的指令——对于如今的浏览器来说,这些指令是有其特定的含义的,换句话说,认为这些表达式没有任何作用是错误的。
为了从返回结果中去掉这些指令,我们需要:
· 扩展where子句,加入额外的限制条件,具体如下所示:
and not e.getParent() instanceof Directive
这样的话,我们的where子句就变成了下面的样子:
where e.isPure() and e.getParent() instanceof ExprStmt and not e.getParent() instanceof Directive
· 之后,再次点击“Run”按钮。由于use strict指令被排除掉了,所以,返回结果的数量会有所减少。
上面我们简单介绍了如何利用查询控制台分析JavaScript项目,下面我们开始介绍用于分析JavaScript代码的CodeQL标准库。
用于分析JavaScript代码的CodeQL标准库
为了帮助人们分析JavaScript代码,CodeQL平台专门提供了一个功能丰富的标准库,来帮助我们分析从JavaScript项目中提取的CodeQL数据库。这个库中的类,不仅能够以面向对象的形式表示数据库中的数据,同时,该库还提供了许多抽象类和谓词,来帮助我们完成各种常见的任务。这个库是通过一组QL模块(即扩展名为.qll的文件)的形式来实现的。其中,模块javascript.qll的作用是导入这个库的其他模块。因此,在编写查询代码时,我们可以在其开头部分加入如下所示的语句来导入完整的库:
import javascript
接下来,我们开始介绍这个库中的一些重要的类和谓词。
概述
总的来说,这个用于分析JavaScript代码的CodeQL标准库,提供了从不同级别上描述JavaScript源代码各种信息的类。这些类可以大体分为如下所示的类型:
· 文本类型:将源代码表示为非结构化文本文件的类
· 词法类型:将源代码表示为一系列单词和注释的类
· 语法类型:将源代码表示为抽象语法树的类
· 名称绑定类型:表示作用域和变量的类
· 控制流类型:表示控制流的类
· 数据流类型:对JavaScript源代码中的数据流进行分析的类
· 类型推断类型:用于推断JavaScript表达式和变量的类型的类
· 调用图类型:表示函数之间调用者-被调用者关系的类
· 过程间数据流类型:可以用来定义进程间数据流和进行污点跟踪分析的类
· 框架类型:表示源代码实体的类,这些实体对JavaScript工具和框架具有特殊意义
请注意,文本级别以上的表示形式(例如,表示词法或流程图的类)仅适用于不包含致命语法错误的JavaScript代码。对于具有此类错误的代码,唯一可用的信息就是文本级别的信息,以及有关错误本身的信息。
此外,CodeQL平台还提供了用于处理HTML文档、JSON和YAML数据、JSDoc注释和正则表达式的标准库。
文本级别
在最低的级别上,我们可以把JavaScript代码简单看成是组织在文件夹中的文件集合,其中每个文件由零行或多行文本组成。
需要注意的是,程序的文本内容并没有包含在CodeQL数据库中,除非我们在提取过程中专门进行了相应的处理。特别是,LGTM平台上的数据库(也称为“快照”)通常不包括文本信息。
文件与文件夹
在CodeQL库中,可以使用File类的实例来表示文件,文件夹则表示为类Folder的实例,它们都是类Container的子类。
类Container为我们提供了以下成员谓词:
· Container.getParentContainer() ,该谓词用于返回文件或文件夹的父文件夹。
· Container.getAFile(),该谓词用于返回文件夹中的一个文件。
· Container.getAFolder() ,该谓词用于返回嵌套在文件夹中的文件夹。
我们知道,文件和文件夹都具有相应的路径,而这些路径都可以通过谓词Container.getAbsolutePath()进行访问。例如,如果f表示一个路径为/home/user/project/src/index.js的文件,那么,谓词f.getAbsolutePath()的返回结果就是字符串“/home/user/project/src/index.js”,而谓词f.getParentContainer().getAbsolutePath()的返回结果为“/home/user/project/src”。
我们看到,上面返回的路径都是文件系统的绝对路径。如果要获取文件相对于CodeQL数据库中的源代码位置的路径,则需要使用谓词Container.getRelativePath()。但是,请注意,数据库可能包含源代码位置之外的文件;对于这样的文件来说,谓词getRelativePath()不会返回任何内容。
此外,Container类的下列成员谓词能够为我们提供有关文件或文件夹名称的详细信息:
· 谓词Container.getBaseName()用于返回文件或文件夹的基本名称,其中不包括其父文件夹,但包括扩展名。对于前面的示例来说,f.getBaseName()将返回字符串“index.js”。
· 谓词Container.getStem()的作用类似于Container.getBaseName(),但返回结果中不包括文件扩展名;因此对于前面的示例来说,f.getStem()将返回字符串“index”。
· 谓词Container.getExtension()用于返回文件扩展名,不包括点号;因此,对于前面的例子来说,f.getExtension()将返回字符串“js”。
例如,以下查询将计算每个文件夹中JavaScript文件(即扩展名为js的文件)的数量:
import javascript from Folder d select d.getRelativePath(), count(File f | f = d.getAFile() and f.getExtension() = "js")
下面是上述代码的返回结果:
位置
CodeQL数据库中的大多数实体都有一个相关联的源代码位置。该位置由四条信息进行标识:一个文件、一个起始行、一个起始列、一个结束行和一个结束列。需要注意的是,这里的行和列都是从1开始计数的(因此,文件的第一个字符位于第1行第1列)。
与源代码位置关联的所有实体都属于Locatable类。而位置本身则是由类location进行表示的,我们可以通过成员谓词Locatable.getLocation()来访问。此外,Location类提供了如下所示的成员谓词:
· 谓词Location.getFile()、Location.getStartLine()、Location.getStartColumn()、Location.getEndLine()、Location.getEndColumn()用于返回与该位置有关的详细信息。
· 谓词Location.getNumLines()用于返回该位置覆盖的(全部或部分)行数。
· 谓词Location.startsBefore(Location)和Location.endsAfter(Location)用于确定一个位置是否在另一个位置之前开始,或在另一个位置之后结束。
· 谓词Location.contains(Location)用于指示一个位置是否完全包含另一个位置;当且仅当谓词· 1.startsBefore(l2) 和谓词 l1.endsAfter(l2)都成立时,谓词l1.contains(l2)才成立。
行数
文件中文本的行数,是由类 Line表示的。这个类提供了如下所示的成员谓词:
· 谓词Line.getText()用于返回某行中的文本,其中不包括终止换行符。
· 谓词Line.getTerminator()用于返回某行的终止符。当然,某些文件的最后一行可能没有包含终止符,在这种情况下,这个谓词不会返回任何内容;否则,它要么返回一个双字符字符串“\r\n”(回车后跟换行符),要么返回一个单字符字符串“\n”(换行符)、“\r”(回车符)、“\u2028”(Unicode字符中的行分隔符)或“\u2029”(Unicode字符中的段分隔符)。
请注意,就像前面说过的那样,程序的文本表示默认情况下是不包含在CodeQL数据库中的。
词法级别
类Token和Comment用于在文本级别之上的词法级别来描述JavaScript程序的结构,它们分别表示单词(token,这里的单词是一个字符串,是构成源代码的最小单位)和注释。
单词
类Token最重要的成员谓词如下所示:
· 谓词Token.getValue()用于返回单词的源文本。
· 谓词Token.getIndex()用于返回单词在脚本中的索引。
· 谓词Token.getNextToken()和Token.getPreviousToken()用于在单词之间切换。
Token类共有九个子类,每个子类代表特定类型的单词。这些子类包括:
· EOFToken:用于表示脚本结尾的标记单词
· NullLiteralToken、BooleanLiteralToken、NumericLiteralToken、StringLiteralToken和RegularExpressionToken:它们用于表示不同类型的文字
· IdentifierToken and KeywordToken:分别表示标识符和关键字(包括保留字)
· PunctuatorToken:用于表示运算符和其他标点符号
下面,我们将给出一个完全在词法级别上操作的查询示例,该查询旨在查找数组表达式中由省略的元素而导致的连续逗号:
import javascript class CommaToken extends PunctuatorToken { CommaToken() { getValue() = "," } } from CommaToken comma where comma.getNextToken() instanceof CommaToken select comma, "Omitted array elements are bad style."
注释
类Comment及其子类可用于表示JavaScript程序中各种类型的注释:
· 类Comment:任意类型的注释
- 子类LineComment:以行尾字符结束的单行注释
- 子类SlashSlashComment:以“//”开头的JavaScript单行注释
- 子类HtmlLineComment:一个(非标准的)HTML注释
· 子类HtmlCommentStart:以“<!—”开头的HTML注释
- 子类HtmlCommentEnd:以“-->”结尾的HTML注释
· 类BlockComment:可跨多行的块注释
- 子类SlashStarComment:位于/*...*/之间的JavaScript块注释
- 子类DocComment:位于/**...*/之间的文档块注释
下面,我们介绍类Comment中最重要的几个成员谓词:
· 谓词Comment.getText(),用于返回注释的源文本,其中不包括分隔符。
· 谓词Comment.getLine(i),用于返回注释中的第i行文本(i从0开始计数)。
· 谓词Comment.getNumLines(),用于返回注释中的行数。
· 谓词Comment.getNextToken(),用于返回紧跟在注释后面的单词。注意,这样的单词始终存在:如果一个注释出现在文件的末尾,那么它的后面的单词就是一个EOFToken。
下面是一个仅使用词法信息的查询示例,其作用是查找HTML注释,由于这些注释并不是标准的ECMAScript特性,所以应该避免使用,具体代码如下所示:
import javascript from HtmlLineComment c select c, "Do not use HTML comments."
下面是其返回结果,这里找到了三个HTML注释:
小结
从本文开始,我们将为读者介绍如何利用CodeQL的标准库来分析JavaScript和TypeScript语言编写的代码中的潜在安全漏洞。首先,我们展示了如何通过LGTM查询控制台来编写和运行分析JavaScript和TypeScript代码的查询。然后,我们讲解了用于分析JavaScript代码的CodeQL标准库中从文本级别和词法级别分析源代码的常用类及其谓词。
备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。
参考资料:https://help.semmle.com/
发表评论