跳转至
#代码审计  #codeql 
本文阅读量 

2.CodeQL语法#

前言#

CodeQL的很多语法和现在的主流高级语言有很多相似之处,但也有许多的不同,学习的时候需要注意。
举一个简单的例子,在CodeQL中不存在==,只有=,当一个变量定义了而没有初始化的时候,=的意思是赋值,但当其已经被赋值了之后,=的意思就变成了比较。

基础数据类型(Primitive types)#

CodeQL 是一种静态类型的语言,因此每个变量都必须有一个声明的类型。类型是一组值。例如,int 类型是一组整数。注意,一个值可以属于这些集合中的多个,这意味着它可以有多个类型。
整型(int),浮点型(float),日期型(date),字符型(stirng),布尔型(boolean),简单介绍下日期型和布尔型。

日期型(date)#

日期型变量用于保存公历表示的时间值和日期值,如年、月、日、时、分、秒以及毫秒等,注意,它们的取值都是整数。其中,表示年的整数的取值范围是从-16777216到16777215,表示月的整数的取值范围为从0到11,表示日的整数的取值范围是从1到31,表示时的整数的取值范围是从0到23,表示分的整数的取值范围是从0到59,表示秒的整数的取值范围是从0到59,表示毫秒的整数的取值范围是从0到999。

编写一个简单的实例用于计算从今年9月1日到今天(11月2日)一共过了多久:

from date start, date end
where start = "01/09/2021".toDate() and end = "02/11/2021".toDate()
select start.daysTo(end)


这里我们用到了字符串的一个内置函数toDate(),更多的相关函数可以查阅CodeQL文档
- string - go
- date - go

布尔型(boolean)#

布尔型变量用来存放布尔值,即false(假)或者 true(真)。
编写一个简单的例子来实现两个布尔之间的和关系:

from boolean a, boolean b
where a = true and b = false
select a.booleanAnd(b)

更多相关函数可以查阅CodeQL文档
- boolean - go

谓词(Predicates)#

谓词有点类似于其他语言中的函数,但又与函数不同,谓词用于描述构成 QL 程序的逻辑关系。确切的说,谓词描述的是给定参数与元组集合的关系。
定义谓词有以下几个注意点(坑点):
1. 需要注意的是谓词的名字开头必须是小写字母。
2. 绑定行为与绑定集,这个在后面会介绍。

无结果谓词#

没有结果的谓词以predicate作为开头,剩下的语法结构类似于定义函数。这种谓词只能在where语句中使用
一个简单的例子如下:

predicate isCity(string city) {
city = "Beijing"
or
city = "ShangHai"
}

from string city
where city = "Beijing" and isCity(city)
select city

结果谓词#

有结果的谓词的定义类似于c/c++语言的函数定义,以返回类型替代predicate作为开头。这种谓词可以在where与select语句中使用
一个简单的例子如下:

int addOne(int i) {
    result = i + 1 and
    i in [1 .. 10]
}

from int v
where v = 1
select addOne(v)

递归谓词#

这里说的递归并非我们常规理解的函数递归,我们可以理解为一个reverse(可反向查找的)谓词,或者换一个思维,把非递归的结果谓词理解为一个有向图,那么递归的结果谓词可以理解为一个无向图。
一个简单的例子如下:

string getANeighbor(string country) {
    country = "France" and result = "Belgium"
    or
    country = "France" and result = "Germany"
    or
    country = "Germany" and result = "Austria"
    or
    country = "Germany" and result = "Belgium"
    or
    country = getANeighbor(result)
  }

from string people
where people = getANeighbor("Germany")
select people


可以看到查询到三个结果,FranceAustriaBelgium,查询到France的原因就是它可以反向查找。

特征谓词,非成员谓词,成员谓词#

谓词分为三种,即非成员谓词、成员谓词和特征谓词。
非成员谓词是在类之外定义的,也就是说,它们不是任何类的成员,而成员谓词则是在类里面定义的。特征谓词则是类中的特殊谓词,类似于其他语言中类的构造函数。
下面是每种谓词的示例:

int getSuccessor(int i) {  // 1. Non-member predicate
  result = i + 1 and
  i in [1 .. 9]
}

class FavoriteNumbers extends int {
  FavoriteNumbers() {  // 2. Characteristic predicate
    this = 1 or
    this = 4 or
    this = 9
  }

  string getName() {   // 3. Member predicate for the class `FavoriteNumbers`
    this = 1 and result = "one"
    or
    this = 4 and result = "four"
    or
    this = 9 and result = "nine"
  }
}

绑定行为与绑定集#

谓词所描述的集合通常不允许是无限的,换句话说,谓词只能包含有限数量的元组(It must be possible to evaluate a predicate in a finite amount of time, so the set it describes is not usually allowed to be infinite. In other words, a predicate can only contain a finite number of tuples.)
举个简单的正例和反例:

// 正例,i被限定在1到10内,或者你也可以给i赋一个确定的值如i=1
int addOne(int i) {
    result = i + 1 and
    i in [1 .. 10]
}

// 反例,i是无限数量值的,此时CodeQL编译器会报错: 'i' is not bound to a value
int addOne(int i) {
    result = i + 1 and
    i > 0
}

单个绑定集#

为了使上述的反例谓词能够通过编译,我们可以使用绑定集(bindingset),但是当我们去调用这个谓词时,传递的参数还是只能在有限的参数集中。
上面的反例可以修改为如下:

bindingset[i]
int addOne(int i) {
    result = i + 1 and
    i > 0
}

// 此时我们可以去调用这个谓词,但是需要注意传递过来的参数还是只能在有限的参数集中
from int i
where i = 1
select addOne(i)

多个绑定集#

我们同样可以添加多个绑定集,下面是一个例子:

bindingset[x] bindingset[y]
predicate plusOne(int x, int y) {
  x + 1 = y
}

这个绑定集的意思是如果x或y绑定(bound)了,那么x和y都绑定,即至少有一个参数受到约束。
如果我们想要两者都受约束,可以将例子修改一下:
bindingset[x, y]
predicate plusOne(int x, int y) {
  x + 1 = y
}

那么这个谓词就变为了一个类似于校验的函数,即x+1 == y

查询(Query)#

查询是CodeQL的输出。查询有两种类型,分别是
- select子句
- 查询谓词,这意味着我们可以在当前模块中定义或者从其他模块中导入

select子句#

select子句的格式如下:

[from] /* ... variable declarations ... */
[where] /* ... logical formula ... */
select /* ... expressions ... */

其中from和where语句是可选的。我们可以在from中定义变量,在where中给变量赋值和对查询结果的过滤,最后在select中显示结果。
在select语句中我们还可以使用一些关键字:
- as关键字,后面跟随一个名字。作用相当于sql中的as,为结果列提供了一个"标签",并允许在后续的select表达式中使用它们。
- order by关键字,后面跟随一个一个结果列名。作用相当于sql中的order by,用于排序结果,并且在结果列名后可选asc(升序)或desc(降序)关键字。

一个简单的例子如下:

from int x, int y
where x = 3 and y in [0 .. 2]
select x, y, x * y as product, "product: " + product

查询谓词#

查询谓词是一个非成员谓词,并在最开头使用query作为注解。它返回谓词计算结果的所有元组,下面是一个简单的示例:

query int getProduct(int x, int y) {
  x = 3 and
  y in [0 .. 2] and
  result = x * y
}

它返回的结果如下:

编写查询谓词而不是select子句的好处是我们可以在代码的其他部分中调用谓词。例如,我们可以在类中的特征谓词内部调用:

query int getProduct(int x, int y) {
  x = 3 and
  y in [0 .. 2] and
  result = x * y
}
class MultipleOfThree extends int {
  MultipleOfThree() { this = getProduct(_, _) }
}

from MultipleOfThree m  
select m

这样我们查询结果就有2个,一个是内置的#select,一个是getProduct#select的结果如下:

类(Classes)#

我们可以在CodeQL中定义自己的类型,一个方法是定义一个类。
类提供了一种简单的方法来重用和构造代码。例如,我们可以:
- 在类中定义成员谓词
- 定义子类以重写成员谓词

类的定义#

定义类的格式如下:

class ClassName [extends Parent] {
      // ...
}

注意这里有个坑点是类名首字母必须是大写。
一个简单的例子如下:
class OneTwoThree extends int {
  OneTwoThree() { // characteristic predicate
    this = 1 or this = 2 or this = 3
  }

  string getAString() { // member predicate
    result = "One, two or three: " + this.toString()
  }

  predicate isEven() { // member predicate
    this = 2
  }
}

在CodeQL中,类允许多重继承,但是以下操作是非法的:
- 不能继承本身
- 不能继承final类
- 不能继承不兼容的类型,请参阅类型兼容性

类的主体#

类的主体可以包含以下内容
- 一个特征谓词
- 任意数量的成员谓词
- 任意数量的字段(field)

在类中,我们可以使用this来指代类本身。当我们定义类时,该类还会从其父类继承所有非私有成员谓词和字段,我们可以覆盖(override)这些谓词和字段。

特征谓词#

类似于其他语言中类的构造函数,只能定义一个,我们可以在特征谓词中使用this来限制类中可能的值。在上述例子中,OneTwoThree被限制为1-3中的整数。

成员谓词#

这些谓词仅适用于类中。我们可以这样去调用上述类的成员谓词:

(OneTwoThree).getAString()
// 结果是 One, two or three: 1

字段(Field)#

字段是在类的主题中声明的变量,一个类的主题中可以有任意数量的字段声明。我们可以在类中的谓词适用这些变量,用法和this类似,字段必须受限于特征谓词。
一个简单的例子如下,它输出10以内每个数字的除数:

class SmallInt extends int {
  SmallInt() { this = [1 .. 10] }
}

class DivisibleInt extends SmallInt {
  SmallInt divisor;   // declaration of the field `divisor`
  DivisibleInt() { this % divisor = 0 }

  SmallInt getADivisor() { result = divisor }
}

from DivisibleInt i
select i, i.getADivisor()

具体类#

上面的例子都是具体类,具体类是通过限制较大类型中的值来定义的。

抽象类#

抽象类使用关键字abstract放在关键字class前来定义。抽象的概念相信在许多其他语言中我们都有接触到(例如java)。抽象类我们又可以叫做元类,它定义其子类的谓词和字段。

一个简单的例子如下:

import go

abstract class SqlExpr extends Expr {
int ID;
}

class PostgresSqlExpr extends SqlExpr {
PostgresSqlExpr(){ ID=1 }
}

class MySqlExpr extends SqlExpr {
MySqlExpr(){ ID=2 }
}

重写成员谓词#

我们使用关键字override来重写一个成员谓词,子类重写的成员谓词会影响父类的成员谓词。
例如,我们可以扩展上面的OneTwoThree类:

class OneTwo extends OneTwoThree {
  OneTwo() {
    this = 1 or this = 2
  }

  override string getAString() {
    result = "One or two: " + this.toString()
  }
}

那么假如我们编写以下查询:
from OneTwoThree o
select o, o.getAString()

得到的结果如下:

在CodeQL中,与其他语言不同,相同父类的子类不会互相冲突。例如,我们可以定义一个TwoThree,它同样继承于OneTwoThree,它的特征谓词的一部分与OneTwo重叠。
class TwoThree extends OneTwoThree {
  TwoThree() {
    this = 2 or this = 3
  }

  override string getAString() {
    result = "Two or three: " + this.toString()
  }
}

现在整数2既是TwoThree也是OneTwo的特征,这两个类都覆盖了原始父类的成员谓词getAString,那么我们重新执行上述查询,在遇到2时会得到2个结果:

多重继承#

在CodeQL中一个类可以继承多个类型,例如:

class Two extends OneTwo, TwoThree {}

这个类Two同时继承了OneTwoTwoThree,它还间接继承自OneTwoThreeint,那么通过特征谓词我们知道其值只能是2。
如果一个子类继承了同一个谓词的多个定义,那么该类还需要手动重写该谓词以避免歧义,在这种情况下我们可以考虑使用super表达式,例如:
class Two extends OneTwo, TwoThree {
  override string getAString() {
    result = TwoThree.super.getAString()
  }
}

### 非继承子类(Non-extending subtypes)
除了使用关键字extends之外,我们还可以使用instanceof来"继承"父类,将一个类声明为instanceof Foo大致等价于在特征谓词中声明了this instanceof Foo,主要的区别是其可以通过super来调用父类的方法。
需要注意的是,在使用关键字instanceof"继承"父类时,使用override关键字重写成员谓词不会对父类造成影响。

## 模块(Modules)
模块提供了一种通过将相关类型、谓词和其他模块组合在一起来组织 QL 代码的方法。
我们可以将模块导入到其他文件中,这样可以避免重复,并有助于代码管理。

### 模块主体
模块中允许包含以下的结构:
- 导入模块语句
- 谓词
- 类型
- 别名
- 显式模块
- select子句(仅可在查询模块中使用)

### 定义模块
直接使用关键字module来显式定义一个模块,一个简单的例子(example.qll)如下:
``ql module Example { class OneTwoThree extends int { OneTwoThree() { this = 1 or this = 2 or this = 3 } } }

### 导入模块
我们可以使用关键字`import`导入模块,这会将命名空间中的所有名称(除私有名称外)都导入当前模块的命名空间里,导入模块语句的形式如下:
```ql
import <module_expression1> as <name>
import <module_expression2>
这相当于python中的from import *`

模块种类#

文件模块#

每个查询文件(.ql)或库文件(.qll)都隐式地定义了一个模块,其模块名与文件同名,但文件中的任何空格都被转换为下划线

库模块#

库模块由.qll文件定义,它可以包含除了select字句之外的模块主体。

查询模块#

库模块存在于库文件(.qll)中,它可以包含除了select字句之外的模块主体。
查询模块由.ql文件定义。它可以包含以下模块主体中列出的任何元素。
查询模块与其他模块稍有不同:
- 无法导入其他查询模块
- 一个查询模块必须包含一个查询,它可以是select语句或者一个查询谓词

显式模块#

我们还可以在一个模块中显式地定义另外一个模块。例如,我们可以在上述的例子(example.qll)中再定义一个模块,例子如下:

...
module M {
  class OneTwo extends OneTwoThree {
    OneTwo() {
      this = 1 or this = 2
    }
  }
}

回到页面顶部