我们正在寻找没有静态分析器的PHP代码中的错误

在静态代码分析中,我最喜欢的部分是提出有关代码中潜在错误的假设,然后进行检查。


假设示例:


 strpos      . 

但是,即使在几百万行代码中,这样的诊断也有可能无法“解决”问题,因此您不希望在不成功的假设上花费大量时间。


今天,我将展示如何使用phpgrep实用程序执行最简单的静态分析,而无需编写代码。



下切:


  • 搜索和分析开源项目中的错误。
  • 通过phpgrep快速入门。
  • 句法搜索的原理。




背景知识


几个月来,我一直在支持NoVerify PHP linter(在VVerntak团队NoVerify文章:PHP的Linter中进行了阅读 )。


团队中会不时出现新诊断的想法。 可能有很多想法,但是我想检查所有内容,特别是如果提议的检查旨在确定关键缺陷的话。


以前,我一直在积极开发go-critic ,情况也差不多,唯一的不同是源代码是在Go中分析的,而不是在PHP中分析的。 当我发现gogrep实用程序时,我的世界颠倒了。 顾名思义,该实用程序与grep有一些共同点,只是搜索不是通过正则表达式执行,而是通过语法模式执行(我将在后面解释这意味着什么)。


我不想没有聪明的grep,所以一个晚上我决定坐下来写phpgrep


分析案例


为了娱乐,我们立即将自己沉浸在应用程序中。 我们将分析GitHub上一小部分相当知名的大型PHP项目。


我们的工具包包括以下项目:



对于正在绘制我们所绘制内容的人来说,这是一个非常令人垂涎的场景。


所以走吧!


使用赋值作为表达式


此外,如果将赋值用作表达式,则:


  • 上下文期望逻辑运算(逻辑条件)的结果,并且
  • 表达式的右边没有副作用,并且是常数,

这很可能是代码错误。


首先,让我们对“逻辑上下文”进行以下构造:


  1. if ($cond) ”中的表达式。
  2. 三元运算符的条件是:“ $cond ? $x : $y ”。
  3. 循环“ while ($cond) ”和“ for ($init; $cond; $post) ”的连续条件。

在赋值的右侧,我们期望常量或文字。


为什么我们需要这样的限制?
这两个限制都是必需的,以减少误报的数量。

如果我们仅在条件内查找分配,则“ ==”暗示有错字而不是“ =”的可能性更高。

但是即使在这种情况下,仍然存在争议的情况,即由于函数内可能发生的副作用而使分配有意义。 为了不处理它们,我们将仅选择最简单的分配值。

让我们从(1)开始:


 #      # |      (  ) # | | # | | phpgrep . 'if ($_ = []) $_' # 1 # | # | #    #  3    . phpgrep . 'if ($_ = ${"const"}) $_' # 2 phpgrep . 'if ($_ = ${"str"}) $_' # 3 phpgrep . 'if ($_ = ${"num"}) $_' # 4 

在这里,我们看到4种模式,它们之间的唯一区别是分配的表达式(RHS)。 让我们从第一个开始。


模板“ if ($_ = []) $_ ”捕获一个if ,该if具有分配给任何表达式的空数组。 $_匹配任何表达式或语句。


     (RHS) | if ($_ = []) $_ | | |   if',   ,  {}    LHS   

以下示例使用更复杂的conststrnum组。 与$_不同$_它们描述了对兼容操作的限制。


  • const是命名常量或类常量。
  • str是任何类型的字符串文字。
  • num是任何类型的数字文字。

这些模式足以实现对外壳的多种操作。


⎆moodle /块/ rss_client / viewfeed.php#L37


 if ($courseid = SITEID) { $courseid = 0; } 

情绪高涨的第二个触发因素是ADOdb依赖性。 在上游库中,问题仍然存在。


⎆ADOdb /驱动程序/ adodb-odbtp.inc.php#L741



这个片段有很多,但对我们来说只有第一行是相关的。 而不是比较databaseType字段,我们执行分配并始终进入条件。


另一个有趣的地方,我们只想对“正确”的记录执行操作,而是始终执行它们,并且将任何记录标记为正确!


⎆moodle /问题/格式/blackboard_six/formatqti.php#L598


 // For BB Fill in the Blank, only interested in correct answers. if ($response->feedback = 'correct') { // ... } 

此检查模板的扩展列表
  phpgrep。  “对于($ _; $ _ = []; $ _)$ _”
 phpgrep。  “用于($ _; $ _ = $ {“ const”}; $ _)$ _”
 phpgrep。  “对于($ _; $ _ = $ {“ num”}; $ _)$ _”
 phpgrep。  “对于($ _; $ _ = $ {“ str”}; $ _)$ _”
 phpgrep。  “同时($ _ = [])$ _”
 phpgrep。  'while($ _ = $ {“ const”})$ _'
 phpgrep。  'while($ _ = $ {“ num”})$ _'
 phpgrep。  'while($ _ = $ {“ str”})$ _'
 phpgrep。  '如果($ _ = [])$ _'
 phpgrep。  '如果($ _ = $ {“ const”})$ _'
 phpgrep。  '如果($ _ = $ {“ str”})$ _'
 phpgrep。  '如果($ _ = $ {“ num”})$ _'
 phpgrep。  '$ _ = []?  $ _:$ _'
 phpgrep。  '$ _ = $ {“ const”}?  $ _:$ _'
 phpgrep。  '$ _ = $ {“ str”}?  $ _:$ _'
 phpgrep。  '$ _ = $ {“ num”}?  $ _:$ _'
 phpgrep。  '(($ _ = [])&& $ _'
 phpgrep。  '($ _ = $ {“ const”})&& $ _'
 phpgrep。  '($ _ = $ {“ str”})&& $ _'
 phpgrep。  '($ _ = $ {“ num”})&& $ _'
 phpgrep。  '$ _ && $ _ = []'
 phpgrep。  '$ _ && $ _ = $ {“ const”}'
 phpgrep。  '$ _ && $ _ = $ {“ str”}'
 phpgrep。  '$ _ && $ _ = $ {“ num”}'
 phpgrep。  '($ _ = [])||  $ _'
 phpgrep。  '($ _ = $ {“ const”})||  $ _'
 phpgrep。  '($ _ = $ {“ str”})||  $ _'
 phpgrep。  '($ _ = $ {“ num”})||  $ _'
 phpgrep。  '$ _ ||  $ _ = []'
 phpgrep。  '$ _ ||  $ _ = $ {“ const”}'
 phpgrep。  '$ _ ||  $ _ = $ {“ str”}'
 phpgrep。  '$ _ ||  $ _ = $ {“ num”}' 

让我们重复一下我们学到的东西:


  • 模板看起来像他们找到的php代码。
  • $_代表任何东西。 您可以与比较. 在正则表达式中。
  • ${"<class>"}$_相似,但具有AST元素类型限制。

还值得强调的是,除变量外的所有内容均按字面映射。 这意味着模式“ array(1, 2 + 3) ”只能由语法结构相同的代码来满足(空格不受影响)。 另一方面,模式“ array($_, $_) ”满足任何两个元素的数组文字。


与自己比较表情


与自己进行比较的需求非常罕见。 它可能是NaN检查,但至少有一半时间是复制/粘贴错误。


⎆Wikia /应用程序/扩展/ SemanticDrilldown /包含/ SD_FilterValue.php#L103


 if ( $fv1->month == $fv1->month ) return 0; 

右边应该是“ $fv2->month ”。


为了表示模板中的重复部分,我们使用名称不是“ _ ”的变量。 模式中的重复机制类似于正则表达式中的反向链接


模式“ $x == $x ”将恰好是上面的示例。 除了“ x ”,可以使用任何名称。 名称相同是很重要的。 捕获时,具有可分辨名称的模板变量不需要具有相同的内容。


使用“ $x <= $x ”找到以下示例。


up Drupal /核心/模块/视图/测试/ src /单元/ ViewsDataTest.php#L166


 $prev = $base_tables[$base_tables_keys[$i - 1]]; $current = $base_tables[$base_tables_keys[$i]]; $this->assertTrue( $prev['weight'] <= $current['weight'] && $prev['title'] <= $prev['title'], // <--------------  'The tables are sorted as expected.'); 

子表达式重复


现在我们知道重复子表达式的可能性,我们可以组成许多有趣的模式。


我的最爱之一是“ $_ ? $x : $x ”。
这是具有正确/错误分支的三元运算符。


⎆joomla -cms /库/ src /用户/ UserHelper.php#L522


 return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; 

两个分支都是重复的,这表明代码中存在潜在的问题。 如果我们看一下周围的代码,就可以理解应该是什么。 为了便于阅读,我删去了部分代码,并将$encrypted变量的名称简化为$enc


 case 'crypt-blowfish': return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); case 'md5-base64': return ($show_encrypt) ? '{MD5}' . $enc : $enc; case 'ssha': return ($show_encrypt) ? '{SSHA}' . $enc : $enc; case 'smd5': return ($show_encrypt) ? '{SMD5}' . $enc : $enc; case 'sha256': return ($show_encrypt) ? '{SHA256}' . $enc : '{SHA256}' . $enc; default: return ($show_encrypt) ? '{MD5}' . $enc : $enc; 

我敢打赌,代码需要以下补丁:


 - ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted; 

PHP中的危险操作优先级


PHP中的一个很好的预防措施是在有正确的计算顺序重要的地方使用分组括号。


在许多编程语言中,表达式“ x & mask != 0 ”具有直观含义。 如果mask描述一个位,则此代码检查x该位不等于零。 不幸的是,对于PHP,此表达式的计算方式如下:“ x & (mask != 0) ”,这几乎始终不是您所需要的。


WordPress,Joomla和moodle使用SimplePie


⎆SimplePie /库/ SimplePie / Locator.php#L254
⎆SimplePie /库/ SimplePie / Locator.php#L384
⎆SimplePie /库/ SimplePie / Locator.php#L412
⎆SimplePie /库/ SimplePie / Sanitize.php#L349
⎆SimplePie /库/ SimplePie.php#L1634


 $feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 

SIMPLEPIE_FILE_SOURCE_REMOTE定义为1 ,因此表达式等同于:


 $feed->method & (1 === 0) // => $feed->method & false 

样本搜索模板
  phpgrep。  '$ x&$ mask == $ y'
 phpgrep。  '$ x&$ mask === $ y'
 phpgrep。  '$ x&$ mask!== $ y'
 phpgrep。  '$ x&$ mask!= $ y'
 phpgrep。  '$ x |  $ mask == $ y'
 phpgrep。  '$ x |  $ mask === $ y'
 phpgrep。  '$ x |  $ mask!== $ y'
 phpgrep。  '$ x |  $ mask!= $ y' 

继续讨论意外的操作优先级这一主题,您可以阅读有关PHP中三元运算符的信息 。 在habr上,甚至有专门的文章: 三元运算符的执行顺序


有可能用phpgrep找到这样的地方吗? 答案是肯定的


 phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_' 

正则表达式验证的好处


⎆Wikia /应用程序/维护/ Wikia / updateCentralInterwiki.inc#L95


 if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; } 

正如代码作者所设想的那样,我们使用3个选项之一检查URL的一致性。 对不起的符号. 没有屏蔽,这将导致事实,而不是falloutvault.com我们可以在任何域上获取falloutvaultxcom并通过测试。



这不是特定于PHP的错误。 在通过正则表达式执行验证并且要检查的字符串的一部分中包含元字符的任何应用程序中,都有可能忘记在需要的地方转义并获得漏洞。


您可以通过运行phpgrep找到这样的地方:


 phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b' 

我们引入了命名的pat子模式,该模式可捕获任何字符串文字,然后将正则表达式中的过滤器应用于该子模式。


过滤器可以应用于任何模板变量。 除正则表达式外,还有结构运算符=!= 。 完整列表可在文档中找到。


${"*"}捕获任意数量的任何参数,因此我们不必担心preg_match函数的可选参数。


数组文字中的重复键


在PHP中,如果执行以下代码,则不会收到任何警告:


 <?php var_dump(['a' => 1, 'a' => 2]); // : array(1) {["a"]=> int(2)} 

我们可以使用phpgrep找到这样的数组:


 [${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}] 

可以按以下方式解密此模式:“一个数组文字,其中在任意位置至少有两个相同的键。” 表达式${"*"}帮助我们描述一个“任意位置”,允许在我们感兴趣的键之前,之间和之后包含0-N个元素。


⎆Wikia /应用程序/扩展名/ Wikia / WikiaMiniUpload / WikiaMiniUpload_body.php#L23


 $script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), // ... ]; 

在这种情况下,这不是一个严重的错误,但是我知道在大型(100多个元素)数组中复制键至少会引起意外的行为,其中一个键与另一个键的值重叠。




到此结束我们简短的游览,并举例说明。 如果需要更多,请在文章末尾介绍如何获取所有结果。


什么是phpgrep?


大多数编辑器和IDE使用纯文本搜索来搜索代码(如果不是搜索类或变量之类的特殊字符),换句话说就是grep之类的东西。


输入“ $x ”,找到“ $x ”。 正则表达式可能可供您使用,然后您实际上可以尝试使用正则表达式解析PHP代码。 有时,如果您正在寻找非常具体和简单的内容,例如,“带有后缀的任何变量”,它甚至可以工作。 但是,如果该带后缀的变量应该是另一个复合表达式的一部分,则会出现困难。


phpgrep是用于方便地搜索PHP代码的工具,该工具使您可以不使用面向文本的常规代码而是使用可识别语法的模板进行搜索。


语法感知意味着模板语言反映了目标语言,并且不像正则表达式那样对单个字符进行操作。 在格式化代码之前,我们也没有任何区别,只是其结构很重要。


可选内容:快速入门

快速上手


安装方式


针对Linux和Windows的 amd64的现成发行版本 ,但是如果您安装了Go,那么一个命令就可以为您的平台获取一个新的二进制文件:


 go get -v github.com/quasilyte/phpgrep/cmd/phpgrep 

如果$GOPATH/bin在系统$PATH ,则phpgrep命令将立即变为可用。 要验证这一点,请尝试使用-help参数运行命令:


 phpgrep -help 

如果没有任何反应,请找到Go在哪里安装了二进制文件,并将其添加到$PATH环境变量中。


查看$GOPATH一种古老而可靠的方法,即使未明确设置它:


 go env GOPATH 

使用方法


创建一个测试hello.php文件:


 <?php function f(...$xs) {} f(10); f(20); f(30); f($x); f(); 

在上面运行phpgrep


 # phpgrep hello.php 'f(${"x:int"})' 'x!=20' hello.php:3: f(10) hello.php:5: f(30) 

我们发现所有对函数f调用都带有一个参数,该参数的值不等于20。


phpgrep如何工作


为了解析PHP,使用了github.com/z7zmey/php-parser库。 它已经足够好了,但是phpgrep一些局限性来自所用解析器的功能。 尝试使用支架正常工作时,尤其会出现很多困难。


phpgrep的原理很简单:


  • AST是从输入模板构建的,过滤器已分解;
  • 对于每个输入文件,将构建完整的AST树;
  • 我们遍历每个文件的AST,试图找到与模式匹配的子树。
  • 对于每个结果,将应用过滤器列表;
  • 通过筛选器的所有结果都将打印到屏幕上。

最有趣的是两个AST节点如何精确匹配以相等。 有时是微不足道的:一对一,元节点可以捕获多个元素。 元节点的示例是${"*"}${"str"}


结论


谈论phpgrep而不提及PhpStorm的结构搜索和替换 (SSR)是不诚实的。 它们解决了类似的问题,并且SSR具有自己的优势,例如,集成到IDE中,并且phpgrep声称它是一个独立程序,例如在CI上更容易安装。


phpgrep也是一个库,您可以在程序中使用该库来匹配PHP代码。 这对于linter和代码生成特别有用。


如果这个工具对您有用,我会很高兴。 如果本文只是激发您朝着前述SSR的方向看,那也很好。




附加材料


可以在patterns.txt文件中找到用于分析的模式的完整列表。 在此文件旁边,您可以找到phpgrep-lint.sh脚本,该脚本使用模板列表简化了phpgrep的启动。


本文没有提供完整的响应列表,但是您可以通过克隆所有命名的存储库并在它们上运行phpgrep-lint.sh来重现实验。


您可以从测试模板(例如, PVS studio文章)中汲取灵感。 我真的很喜欢逻辑表达式:专业人士犯的错误 ,它会变成这样的东西:


 #  "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b' 

您可能也对phpgrep的演示感兴趣:语法感知代码搜索


本文使用通过gopherkon创建的gophers图像。

Source: https://habr.com/ru/post/zh-CN464893/


All Articles