Semgrep 之模式语法(二)

上一章节介绍了Semgrep使用及相关原理,传送门:Semgrep 之初识

下面这章就开始介绍Semgrep的模式语法,如何检测想要的代码。心急的同学,可以直接使用交互式的 Semgrep 规则教程,很快就能熟悉上手,https://semgrep.dev/learn。

一、Semgrep 规则可以做什么?

工欲善其事,必先利其器,首先要理解规则能做什么,才能更好的编写规则。

1.1 自动化的 Code Review 并在 PR 时评论

Semgrep 之模式语法(二)

1.2 识别违反安全编码规范的代码

例如,检测 React 的规则 dangerouslySetInnerHTML 如下所示。dangerouslySetInnerHTML 是 React 用于代替在浏览器 DOM 中使用 innerHTML,类似 Vue 的 v-html。一般来说,从代码设置 HTML 是有风险的,因为很容易将用户暴露于跨站点脚本(XSS) 攻击。Semgrep 检测识别该代码可以提醒其进行整改,或者接入 js-xss 库来进行防御。Semgrep 之模式语法(二)

1.3 扫描配置文件

Semgrep 原生支持 JSON 和 YAML,可用于为配置文件编写规则。此规则检查 Kubernetes 集群中跳过的 TLS 验证。Semgrep 之模式语法(二)

Semgrep 规则还可以用于更多事情。例如检测即将弃用的 API、强制执行身份验证等等。具体可以参考更多的官方用例:https://semgrep.dev/docs/writing-rules/rule-ideas/

二、模式语法(Pattern syntax)

模式语法介绍了 Semgrep 模式可以做什么,并提供省略号运算符、元变量等的示例用例。

在命令行中,模式是用标志 --pattern(或 -e)指定的。可以在配置文件中指定多个同等模式。

2.1 模式匹配(Pattern matching)

模式匹配搜索给定模式的代码。例如,表达式 1 + func(42) 可以匹配完整的表达式或者是子表达式的一部分:

foo(1 + func(42)) + bar()

同样,return 42 可以匹配函数中的顶部语句或任何嵌套语句:

def foo(x):
  if x > 1:
     if x > 2:
       return 42
  return 42

2.2 省略号运算符

省略号运算符 ( …) 抽象出一系列零个或多个参数、语句、参数、字段、字符串等。

2.2.1 函数调用(Function calls)

使用省略号运算符搜索函数调用或具有特定参数的函数调用。例如,该模式 insecure_function(...) 查找调用而不管其参数如何。

insecure_function("MALICIOUS_STRING", arg1, arg2)

函数和类可以通过它们的名称来引用,例如

  • django.utils.safestring.mark_safe(...) 或者 mark_safe(...)
  • System.out.println(...) 或者 println(...)

还可以在匹配后搜索带参数的调用。该模式 func(1, ...) 将匹配两者:

func(1, "extra stuff", False)
func(1)  *# Matches no arguments as well*

或者在匹配之前查找带有参数的调用 func(..., 1)

func("extra stuff", False, 1)
func(1)  *# Matches no arguments as well*

该模式 requests.get(..., verify=False, ...) 查找参数出现在任何地方的调用:

requests.get(verify=False, url=URL)
requests.get(URL, verify=False, timeout=3)
requests.get(URL, verify=False)

将关键字参数值与模式匹配 $FUNC(..., $KEY=$VALUE, ...)

2.2.2 方法调用(Method calls)

省略号运算符也可用于搜索方法调用。例如,模式 $OBJECT.extractall(...) 匹配:

tarball.extractall('/path/to/directory')  *# 潜在的任意文件覆盖问题*

还可以在方法调用链中使用省略号。例如,模式 $O.foo(). ... .bar() 将匹配:

obj = MakeObject()
obj.foo().other_method(1,2).again(3,4).bar()

2.2.3 函数定义(Function definitions)

省略号运算符可用于函数参数列表或函数体中。要查找具有可变默认参数的函数定义:

pattern: |
  def $FUNC(..., $ARG={}, ...):
      ...

def parse_data(parser, data={}):  *# Oops, mutable default arguments*
    pass

YAML 文件 | 运算符允许多行字符串。

省略号运算符也可用于函数名。实际上,在某些情况下,可能想要匹配任意的函数定义:例如常规函数、方法,还有匿名函数( lambda)。在这种情况下,可以使用省略号代替函数名称来匹配命名或匿名函数。例如,在 Javascript 中,模式 function ...($X) { ... } 将匹配具有一个参数的任何函数:

function foo(a) {
  return a;
}
var bar = function (a) {
  return a;
};

2.2.4 类定义(Class definitions)

省略号运算符可用于类定义。例如当要查找从某个父级继承的类:

pattern: |
  class $CLASS(InsecureBaseClass):
      ...

class DataRetriever(InsecureBaseClass):
    def __init__(self):
        pass

2.2.5 字符串(Strings)

省略号运算符可用于搜索包含任何数据的字符串。例如使用 crypto.set_secret_key("...") 匹配:

crypto.set_secret_key("HARDCODED SECRET")

2.2.6 二元运算(Binary operations)

省略号运算符可以将任意数量的参数匹配到二元运算。模式 $X = 1 + 2 + ... 匹配:

foo = 1 + 2 + 3 + 4

2.2.7 容器(Containers)

省略号运算符可以匹配内部容器内的数据结构,如列表、数组和键值存储。

  • 模式 user_list = [..., 10] 匹配:
user_list = [8, 9, 10]

  • 模式 user_dict = {...} 匹配:
user_dict = {'username''password'}

  • 该模式 user_dict = {..., $KEY: $VALUE, ...} 匹配以下内容并允许进一步的元变量查询:
user_dict = {'username''password''address''zipcode'}

  • 还可以仅匹配字典中的键值对,例如在 JSON 中,模式 "foo": $X 仅匹配以下中的一行:
"bar": True,
  "name""self",
  "foo": 42
}

2.2.8 条件和循环(Conditionals and loops)

省略号运算符可以在条件或循环中使用。例如:

pattern: |
  if $CONDITION:
      ...

将会匹配如下代码:

if can_make_request:
    check_status()
    make_request()
    return

如果后续重用主体语句信息,则元变量可以匹配条件或循环主体。例如:

pattern: |
  if $CONDITION:
      $BODY

将会匹配下面代码:

if can_make_request:
    single_request_statement()

2.3 元变量(Metavariables)

元变量是一种抽象表达。用于在不知道值或内容时匹配代码,类似于正则表达式中的捕获组。

元变量可用于跟踪特定代码范围内的值。包括变量、函数、参数、类、对象方法、导入、异常等。

元变量一般看起来像 $X$WIDGET$USERS_2。都是以x $some_value` 的名称无效。

2.3.1 表达式元变量(Expression metavariables)

例如 $X + $Y 将会与以下代码示例匹配:

foo() + bar()

current + total

2.3.2 导入元变量(Import metavariables)

元变量也可用于匹配导入。例如,import $X 匹配如下示例:

import random

2.3.3 重复出现元变量(Reoccuring metavariables)

重用元变量显示了它的真正威力。例如检测无用的分配:

pattern: |
  $X = $Y
  $X = $Z

检测到下面代码无用的分配:

initial_value = 10  *# Oops, useless assignment*
initial_value = get_initial_value()

2.3.4 字面元变量(Literal Metavariables)

可以使用 "$X" 匹配任何字符串文字。这与 前文介绍的省略号运算符类似。但字符串的内容存储在元变量 $X 中,可以用于在 message 或 metavariable-regex。还可以使用 /$X/ and :$X 来分别匹配任何正则表达式或原子(语言需要支持这些结构,例如 Ruby)。

2.3.5 类型化元变量(Typed Metavariables)

类型化元变量仅在元变量被声明为特定类型时才匹配。例如,你可能想要专门检查 == 从未用于字符串的那个。

Java:
pattern: $X == (String $Y)

public class Example {
    public int foo(String a, int b) {
        *// Matched*
        if (a == "hello") {
            return 1;
        }

        *// Not matched*
        if (b == 2) {
            return -1;
        }
    }
}

C :
pattern: $X == (char *$Y)

int main() {
    char *a = "Hello";
    int b = 1;

    *// Matched*
    if (a == "world") {
        return 1;
    }

    *// Not matched*
    if (b == 2) {
        return -1;
    }

    return 0;
}

Go:
pattern: "$X == ($Y : string)"

func main() {
    var x string
    var y string
    var a int
    var b int

    *// Matched*
    if x == y {
       x = y
    }

    *// Not matched*
    if a == b {
       a = b
    }
}

对于 Go 语言,Semgrep 目前无法识别在同一行声明的所有变量的类型。也就是说,以下不会同时取 ab 作为 int s:var a, b = 1, 2

TypeScript:
pattern: $X == ($Y : string)

function foo(a: string, b: number) {
  *// Matched*
  if (a == "hello") {
    return 1;
  }

  *// Not matched*
  if (b == 1) {
    return -1;
  }
}

使用类型元变量(Using Typed Metavariables),类型推断适用于整个文件!常见方法是检查在特定类型的对象上调用的函数。例如,假设要在这样的类中寻找对潜在不安全记录器的调用:

class Test {
    static Logger logger;

    public static void run_test(String input, int num) {
        logger.log("Running a test with " + input);
        
        test(input, Math.log(num));
    }
}

如果你搜索 $X.log(...),将会匹配 Math.log(num)。相反,可以搜索

(Logger $X).log(...)

这只会给出 logger 结果

由于 Semgrep 只能匹配单个文件,因此只能保证对局部变量和参数有效。此外,Semgrep 目前对类型的理解很浅。例如,如果有 int[] A 类型,它将无法识别 A[0] 为整数。如果有一个带有字段的类,将无法对字段访问使用类型检查,并且它不会将该类的字段识别为预期类型。Literal 类型仅在有限程度上被理解。

2.3.6 省略号元变量(Ellipsis Metavariables)

可以结合省略号和元变量来匹配参数序列,并将匹配的序列存储在元变量中。例如,foo(…arg)将匹配:

foo(1,2,3,1,2)

2.4 等价(Equivalences)

Semgrep 会自动搜索语义等价的代码。

2.4.1  import 导入

使用别名或子模块的等价导入将会被匹配。例如模式 subprocess.Popen(...) 匹配:

import subprocess.Popen as sub_popen
sub_popen('ls')

模式 foo.bar.baz.qux(...) 匹配:

from foo.bar import baz
baz.qux()

2.4.2 常数等价

Semgrep 会检测常数是否等价。例如 set_password("password") 匹配:

HARDCODED_PASSWORD = "password"

def update_system():
    set_password(HARDCODED_PASSWORD)

2.4.3 结合和交换运算符(Associative and Commutative operators)

Semgrep 执行关联交换 (AC) 匹配。

例如,... && B && C 将同时匹配 B && C and (A && B) && C(即 && 是关联的)。

此外,A | B | C 将匹配 A | B | C, and B | C | A, and C | B | A, 和任何其他排列(即,| 是关联的和可交换的)。

在 AC 匹配下,元变量的行为类似于 ...。例如,可以通过四种不同的方式 A | $X 进行匹配($X 可以绑定到 B, 或者 C, 或者 B | C)。为了避免组合过多,Semgrep 只会在潜在匹配的数量很小的情况下对元变量执行 AC 匹配,否则它将只产生一个匹配,其中每个元变量都绑定到单个操作数。

使用 options 它可以完全禁用 AC 匹配。也可以将布尔 AND 和 OR 运算符(例如,&&|| C 系列语言中)视为可交换的,尽管在语义上不准确,但它还是很有用的。

2.5 深度表达式运算符(Deep expression operator)

使用深度表达式运算符 <... [your_pattern] ...> 来匹配可以深度嵌套在另一个表达式中的表达式。例如,下面的 Pattern:

pattern: |
  if <... $USER.is_admin() ...>:
    ...

匹配如下代码:

if user.authenticated() and user.is_admin() and user.has_group(gid):
  [ CONDITIONAL BODY ]

深度表达式运算符适用于:

  • if 声明:if <... $X ...>:
  • 嵌套调用:sql.query(<... $X ...>)
  • 二元表达式的操作:"..." + <... $X ...>
  • 任何其他表达式上下文

三、局限(Limitations)

3.1 语句类型(Statements types)

Semgrep 处理某些语句类型与其他语句方式不同,尤其是在代码语句中搜索片段时。例如,该模式 foo 将匹配以下语句:

x += foo()
return bar + foo
foo(1, 2)

foo 不会匹配以下语句

import foo

许多编程语言区分表达式和语句。表达式可以出现在 if 条件、函数调用参数中。而语句不能;它们是一系列的操作(在许多语言中 ; 用作分隔符/终止符)或特殊的控制流构造(if、while 等)。

foo() 是一个表达式(在大多数语言中)。

foo(); 是一个陈述(在大多数语言中)。

如果你的搜索模式是语句,Semgrep 将自动尝试将其作为表达式和语句进行搜索

当你在模式中编写表达式 foo() 时,Semgrep 将访问程序中的每个表达式和子表达式并尝试找到匹配项。

许多程序员并没有看到 foo()foo(); 之间的区别。这就是为什么当人们寻找 foo(); Semgrep 认为用户想要匹配诸如 a = foo();, 或之类的语句 print(foo());

在某些编程语言中,例如 Python,它不使用分号作为分隔符或终止符,表达式和语句之间的区别更加令人困惑。Python 中的缩进很重要,foo() 后面的换行符实际上与 foo(); 等 C 等其他编程语言相同

3.2 部分表达式(Partial expressions)

部分表达式不是有效的模式。例如,以下内容无效:

pattern: 1+

需要一个完整的表达式(如 1 + $X

3.3 省略号和语句块(Ellipses and statement blocks)

省略号运算符不会内部语句块跳转到外部语句块。

例如,这种 Pattern:

foo()
...
bar()

匹配如下代码:

foo()
baz()
bar()

并且还匹配下面代码:

foo()
baz()
if cond:
    bar()

但它不匹配这个代码

if cond:
    foo()
baz()
bar()

因为 ... 不能从内部语句块 foo() 跳转到外部块 bar()

3.3 部分语句(Partial statements)

不完全地支持部分语句。例如,可以仅将条件的 header 与 匹配 if ($E),或者仅将异常语句的 try 部分与匹配 try { ... }。这在用于 模式内部以限制搜索其他事物的上下文时特别有用。

3.4 其他部分结构体(Other partial constructs)

可以只匹配函数的头部(没有函数体),例如 int foo(...) 只匹配函数的头部部分 foo。同样,您可以只匹配一个类头(例如,with class $A)。


原文始发于微信公众号(洋洋自语):Semgrep 之模式语法(二)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/272935.html

(0)
明月予我的头像明月予我bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!