PHP静态分析的自定义规则:针对项目特有反模式或安全漏洞的检测

PHP静态分析自定义规则:针对项目特有反模式或安全漏洞的检测

大家好,今天我们来聊聊PHP静态分析中自定义规则的应用,特别是针对项目特有的反模式和安全漏洞。静态分析是软件开发中一种重要的质量保证手段,它可以在不实际运行代码的情况下,通过分析代码的结构、数据流和控制流来发现潜在的问题。而自定义规则则允许我们将静态分析工具的能力扩展到特定项目的需求,从而提高代码质量和安全性。

1. 静态分析简介

首先,我们简单回顾一下静态分析的基本概念。

什么是静态分析?

静态分析是指在不执行代码的情况下,通过分析代码的文本表示来检测缺陷、安全漏洞和代码风格问题的技术。它通常包括词法分析、语法分析、语义分析和数据流分析等步骤。

静态分析的优势:

  • 早期发现问题: 在代码提交或部署之前发现问题,降低修复成本。
  • 自动化: 可以自动化执行,减少人工审查的工作量。
  • 覆盖性: 可以覆盖代码的各个分支和执行路径,提高代码覆盖率。
  • 一致性: 可以强制执行代码规范和最佳实践,保持代码风格的一致性。

常用的PHP静态分析工具:

  • PHPStan: 一个专注于发现代码错误的静态分析工具,支持用户自定义规则。
  • Psalm: 另一个流行的静态分析工具,提供强大的类型推断功能和自定义规则支持。
  • Rector: 一个自动化的代码重构工具,也可以用于静态分析和代码规范检查。
  • Phan: 专注于兼容性检查和性能分析的静态分析工具。

2. 为什么需要自定义规则?

虽然现有的静态分析工具已经提供了大量的内置规则,但它们通常是通用的,无法满足所有项目的特定需求。以下是一些需要自定义规则的常见场景:

  • 项目特定的反模式: 某些代码模式在特定项目中可能被认为是反模式,需要被检测和避免。例如,在某个框架中不推荐使用的函数或类。
  • 自定义安全漏洞: 项目可能存在一些特定的安全漏洞,这些漏洞无法被通用的静态分析规则检测到。例如,不安全的API调用或配置错误。
  • 自定义代码规范: 项目可能需要遵循特定的代码规范,这些规范超出了通用代码规范的范围。例如,特定的命名约定或注释格式。
  • 遗留系统: 对于遗留系统,可能需要定制规则来处理一些历史遗留问题,例如废弃的函数或类。
  • 框架/库特定的用法: 针对项目使用的框架或库,可以创建规则来强制使用最佳实践,避免错误用法。

3. 如何编写自定义规则?

接下来,我们将以PHPStan为例,详细介绍如何编写自定义规则。PHPStan允许用户通过实现特定的接口来定义自己的规则。

3.1 PHPStan自定义规则的基本结构

一个PHPStan自定义规则通常包含以下几个部分:

  • Rule类: 实现了PHPStanRulesRule接口的类,包含规则的逻辑。
  • getNodeType()方法: 定义规则需要分析的AST节点类型。
  • processNode()方法: 规则的核心逻辑,用于分析指定的AST节点,并返回错误信息。

3.2 一个简单的例子:禁止使用var_dump()函数

这是一个最简单的例子,禁止在代码中使用var_dump()函数。

<?php

declare(strict_types=1);

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeExprFuncCall;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * 规则:禁止使用 var_dump() 函数
 */
class NoVarDumpRule implements Rule
{
    public function getNodeType(): string
    {
        return FuncCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!($node instanceof FuncCall)) {
            return [];
        }

        if (!($node->name instanceof PhpParserNodeName)) {
            return [];
        }

        if ($node->name->toString() === 'var_dump') {
            return [
                '禁止使用 var_dump() 函数,请使用日志或调试工具代替。',
            ];
        }

        return [];
    }
}

代码解释:

  • NoVarDumpRule 类实现了 PHPStanRulesRule 接口。
  • getNodeType() 方法返回 FuncCall::class,表示该规则只分析函数调用节点。
  • processNode() 方法接收一个 AST 节点和一个 Scope 对象作为参数。Scope 对象提供了关于当前代码上下文的信息,例如变量类型和函数定义。
  • processNode() 方法中,我们首先判断节点是否为 FuncCall 类型,然后判断函数名是否为 var_dump。如果是,则返回一个包含错误信息的数组。

3.3 注册自定义规则

要让PHPStan使用自定义规则,需要在PHPStan的配置文件(通常是 phpstan.neonphpstan.neon.dist)中注册该规则。

includes:
    - vendor/phpstan/phpstan/conf/bleedingEdge.neon

parameters:
    paths:
        - src

    rules:
        - MyProjectPHPStanRulesNoVarDumpRule

配置文件解释:

  • includes 部分引入了 PHPStan 的默认配置。
  • parameters.paths 部分指定了需要分析的代码目录。
  • parameters.rules 部分列出了需要使用的规则。

3.4 测试自定义规则

编写完自定义规则后,需要进行测试,以确保规则能够正确地检测到问题。PHPStan提供了一个方便的测试框架,可以用于测试自定义规则。

首先,需要创建一个测试类,继承自 PHPStanTestingRuleTestCase

<?php

declare(strict_types=1);

namespace MyProjectPHPStanTestsRules;

use MyProjectPHPStanRulesNoVarDumpRule;
use PHPStanRulesRule;
use PHPStanTestingRuleTestCase;

class NoVarDumpRuleTest extends RuleTestCase
{
    protected function getRule(): Rule
    {
        return new NoVarDumpRule();
    }

    public function testRule(): void
    {
        $this->analyse(
            [__DIR__ . '/data/no-var-dump.php'],
            [
                [
                    '禁止使用 var_dump() 函数,请使用日志或调试工具代替。',
                    10, // 错误发生的行号
                ],
            ]
        );
    }

    public static function getAdditionalConfigFiles(): array
    {
        return [__DIR__ . '/../../../extension.neon'];
    }
}

代码解释:

  • NoVarDumpRuleTest 类继承自 PHPStanTestingRuleTestCase
  • getRule() 方法返回需要测试的规则实例。
  • testRule() 方法定义了一个测试用例。
  • $this->analyse() 方法接收一个包含测试代码的文件路径数组和一个包含预期错误的数组作为参数。
  • getAdditionalConfigFiles() 方法返回一个包含额外配置文件的数组,例如包含自定义规则注册的配置文件。

然后,创建一个包含测试代码的文件 data/no-var-dump.php

<?php

function foo() {
    $bar = 123;
    var_dump($bar);
}

最后,运行测试。

./vendor/bin/phpunit --filter NoVarDumpRuleTest

如果测试通过,则表示自定义规则能够正确地检测到 var_dump() 函数的使用。

3.5 进阶:更复杂的规则

上面的例子很简单,只是检测一个函数调用。更复杂的规则可能需要分析更多的AST节点类型,并使用Scope对象来获取更多的代码上下文信息。

例如,假设我们需要创建一个规则,检测是否在循环内部执行了数据库查询操作,这可能会导致性能问题。

<?php

declare(strict_types=1);

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeStmtFor_;
use PhpParserNodeStmtForeach_;
use PhpParserNodeExprMethodCall;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * 规则:禁止在循环内部执行数据库查询
 */
class NoDatabaseQueryInLoopRule implements Rule
{
    public function getNodeType(): string
    {
        return NodeStmt::class; // 监听所有语句,然后在processNode中过滤循环语句
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!($node instanceof For_) && !($node instanceof Foreach_)) {
            return [];
        }

        $errors = [];
        $this->findDatabaseQueryInLoop($node, $scope, $errors);

        return $errors;
    }

    private function findDatabaseQueryInLoop(Node $node, Scope $scope, array &$errors): void
    {
        foreach ($node->getStmts() as $stmt) {
            if ($stmt instanceof MethodCall) {
                // 假设数据库查询操作是通过一个特定的方法调用的,例如 query()
                if ($stmt->name->toString() === 'query') {
                    $errors[] = '禁止在循环内部执行数据库查询,这可能会导致性能问题。';
                }
            }

            // 递归分析子语句
            if ($stmt instanceof NodeStmt) {
                $this->findDatabaseQueryInLoop($stmt, $scope, $errors);
            }
        }
    }
}

代码解释:

  • getNodeType() 方法返回 NodeStmt::class,表示该规则分析所有语句节点。
  • processNode() 方法首先判断节点是否为 For_Foreach_ 循环语句。
  • findDatabaseQueryInLoop() 方法递归分析循环体内的语句,查找数据库查询操作。
  • 如果找到数据库查询操作,则将错误信息添加到 $errors 数组中。

3.6 使用Scope对象

Scope 对象提供了关于当前代码上下文的信息,可以用于获取变量类型、函数定义、类定义等信息。例如,可以使用 Scope->getType() 方法获取变量的类型,可以使用 Scope->getClassReflection() 方法获取当前类的反射信息。

<?php

declare(strict_types=1);

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeExprVariable;
use PHPStanAnalyserScope;
use PHPStanRulesRule;
use PHPStanTypeObjectType;

/**
 * 规则:禁止使用特定的类
 */
class NoSpecificClassRule implements Rule
{
    private string $className;

    public function __construct(string $className)
    {
        $this->className = $className;
    }

    public function getNodeType(): string
    {
        return Variable::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!($node instanceof Variable)) {
            return [];
        }

        $variableType = $scope->getType($node);

        if ($variableType instanceof ObjectType && $variableType->getClassName() === $this->className) {
            return [
                sprintf('禁止使用类 %s,请使用替代方案。', $this->className),
            ];
        }

        return [];
    }
}

代码解释:

  • NoSpecificClassRule 接收一个类名作为参数。
  • processNode() 方法使用 Scope->getType() 方法获取变量的类型。
  • 如果变量类型是 ObjectType 并且类名与指定的类名相同,则返回错误信息。

4. 针对安全漏洞的自定义规则

静态分析可以用于检测各种安全漏洞,例如SQL注入、跨站脚本攻击(XSS)和代码注入。通过自定义规则,可以将静态分析工具的能力扩展到特定项目的安全需求。

4.1 SQL注入

SQL注入是指攻击者通过在用户输入中注入恶意的SQL代码,从而篡改或窃取数据库中的数据。静态分析可以用于检测未经过安全过滤的用户输入,并发出警告。

例如,假设我们有一个使用PDO进行数据库查询的代码,我们需要创建一个规则,检测是否直接将用户输入传递给 PDO::query() 方法。

<?php

declare(strict_types=1);

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeExprMethodCall;
use PhpParserNodeExprVariable;
use PhpParserNodeScalarString_;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * 规则:检测SQL注入风险
 */
class SQLInjectionRule implements Rule
{
    public function getNodeType(): string
    {
        return MethodCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!($node instanceof MethodCall)) {
            return [];
        }

        if (!($node->name instanceof PhpParserNodeIdentifier)) {
            return [];
        }

        if ($node->name->toString() !== 'query') {
            return [];
        }

        // 检查是否是 PDO 的 query 方法
        $callerType = $scope->getType($node->var);
        if (!(string) $callerType instanceof PHPStanTypeObjectType) {
            return [];
        }
        if ((string) $callerType->getClassName() !== 'PDO') {
            return [];
        }

        // 检查参数是否直接来自用户输入
        if (count($node->args) > 0) {
            $arg = $node->args[0]->value;

            if ($arg instanceof Variable) {
                //TODO: 这里可以进一步判断是否用户输入,例如$_GET, $_POST
                return [
                    '存在SQL注入风险:请使用预处理语句或参数绑定来防止SQL注入。',
                ];
            } elseif ($arg instanceof String_) {
                // 如果是字符串常量,则忽略
                return [];
            } else {
               //TODO: 这里可以进一步判断是否用户输入,例如$_GET, $_POST
                return [
                    '存在SQL注入风险:请使用预处理语句或参数绑定来防止SQL注入。',
                ];
            }
        }

        return [];
    }
}

4.2 跨站脚本攻击(XSS)

XSS是指攻击者通过在网页中注入恶意的JavaScript代码,从而窃取用户的信息或篡改网页的内容。静态分析可以用于检测未经过安全转义的用户输出,并发出警告。

例如,假设我们有一个将用户输入直接输出到网页的代码,我们需要创建一个规则,检测是否直接输出用户输入。

<?php

declare(strict_types=1);

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeExprEcho_;
use PhpParserNodeExprVariable;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * 规则:检测XSS风险
 */
class XSSRule implements Rule
{
    public function getNodeType(): string
    {
        return Echo_::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!($node instanceof Echo_)) {
            return [];
        }

        foreach ($node->exprs as $expr) {
            if ($expr instanceof Variable) {
                //TODO: 这里可以进一步判断是否用户输入,例如$_GET, $_POST
                return [
                    '存在XSS风险:请对用户输入进行安全转义,例如使用htmlspecialchars()函数。',
                ];
            }
        }

        return [];
    }
}

5. 代码示例

下面是一个更完整的代码示例,展示了如何创建一个自定义规则,检测是否使用了废弃的函数。

<?php

declare(strict_types=1);

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeExprFuncCall;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * 规则:禁止使用废弃的函数
 */
class NoDeprecatedFunctionRule implements Rule
{
    /**
     * @var array<string, string> 废弃的函数列表
     */
    private array $deprecatedFunctions;

    public function __construct(array $deprecatedFunctions)
    {
        $this->deprecatedFunctions = $deprecatedFunctions;
    }

    public function getNodeType(): string
    {
        return FuncCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!($node instanceof FuncCall)) {
            return [];
        }

        if (!($node->name instanceof PhpParserNodeName)) {
            return [];
        }

        $functionName = $node->name->toString();

        if (array_key_exists($functionName, $this->deprecatedFunctions)) {
            return [
                sprintf(
                    '禁止使用废弃的函数 %s,请使用 %s 代替。',
                    $functionName,
                    $this->deprecatedFunctions[$functionName]
                ),
            ];
        }

        return [];
    }
}

配置示例:

parameters:
    rules:
        -
            class: MyProjectPHPStanRulesNoDeprecatedFunctionRule
            arguments:
                deprecatedFunctions:
                    mysql_connect: mysqli_connect
                    ereg: preg_match

6. 总结

自定义规则是PHP静态分析中一个强大的功能,它可以让我们根据项目的特定需求来扩展静态分析工具的能力,从而提高代码质量和安全性。通过学习本文,你应该已经掌握了如何编写、注册和测试自定义规则的基本方法。希望这些知识能帮助你在实际项目中更好地应用静态分析技术。

总而言之,自定义规则允许我们根据项目特点定制静态分析,从而更有效地发现反模式和安全漏洞,确保代码质量。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注