oro/phpstan-rules

一套用于Oro产品的额外PHPStan规则。

1.8.0 2022-10-17 11:58 UTC

This package is auto-updated.

Last update: 2024-09-10 17:09:25 UTC


README

本包包含一套用于PHPStan - PHP静态分析工具的额外规则。

我们在Oro公司使用这些规则,并要求为Oro产品贡献代码的任何人也遵循它们。

规则

不安全的DQL使用分析

为什么需要检查DQL和SQL查询

使用DQL并不能防止注入漏洞。以下API被设计为能够防止SQL注入

  • 对于Doctrine\DBAL\Connection#insert($table, $values, $types), Doctrine\DBAL\Connection#update($table, $values, $where, $types) 和 Doctrine\DBAL\Connection#delete($table, $where, $types),只有 $values 和 $where 的数组值。表名和 $values 和 $where 的键不会被转义。
  • Doctrine\DBAL\Query\QueryBuilder#setFirstResult($offset)
  • Doctrine\DBAL\Query\QueryBuilder#setMaxResults($limit)
  • Doctrine\DBAL\Platforms\AbstractPlatform#modifyLimitQuery($sql, $limit, $offset) 用于 $limit 和 $offset 参数。

请考虑所有其他API可能不安全,对于用户输入

  • Connection上的查询方法
  • QueryBuilder API
  • Platforms和SchemaManager API用于生成和执行DML/DDL SQL语句
  • 使用各种表达式构建器构建的表达式

详见Doctrine安全完整文章

静态代码分析 - 执行

由于检查整个代码库需要大量时间,因此添加了SQL注入搜索工具以简化过程。该工具基于PHPStan - PHP静态分析工具,并作为附加规则实现。

要检查代码库中不安全的DQL和SQL使用,请执行以下操作

  • 更改目录到 <application_path>/tool/,其中 <application_path> 是文件系统中的应用程序路径
  • 安装依赖项 composer install
  • 使用 ./bin/phpstan analyze -c phpstan.neon <path_to_code> --autoload-file=<path_to_autoload.php> 运行检查

为了加快分析速度,建议按包并行运行。这可以通过 parallel 命令实现

cd my_application/tool/
COMPOSER=composer-sql.json composer install
rm -rf logs;
mkdir logs;
ls ../package/ \
| grep -v "\-demo" | grep -v "demo-" | grep -v "test-" | grep -v "german-" | grep -v "view-switcher" | grep -v "twig-inspector" \
| parallel -j 4  "./bin/phpstan analyze -c phpstan.neon `pwd`/../package/{} --autoload-file=`pwd`/../application/commerce-crm-ee/vendor/autoload.php > logs/{}.log"

请注意,commerce-crm-ee 应用程序应该已生成 autoload.php。分析结果应在一分钟内可用。每个结果都应仔细检查。应小心处理不安全的变量,并对其进行清理或转义。

如何修复发现的警告

不安全的变量是任何可能依赖于外部数据的方法。即使是缓存数据,如果攻击者能够访问缓存存储,也可能是不安全的。任何来自外部或包含由不安全函数返回的值的变量也是不安全的,并且在查询中传递时应谨慎。

您可以通过多种方式使变量安全

ORM

基于ORM的查询可能包含易受攻击的输入。为了保持其清洁,请遵循以下规则

  • 参数标识符 必须 是命名占位符。直接将数据作为右操作数传递是禁止的。唯一的可能例外是数字、\DateTime 和布尔值。
  • 所有标识符必须与 [a-zA-Z0-9_] 兼容。

如果需要直接将变量传递给查询,请使用 QueryBuilderUtil 的安全方法

  • getField - 当字段是用 sprintf 或连接构造时使用它。例如,select($alias . '.' . $field)select('alias.', $field)select(sprintf('%s.%s', $alias, $field) 等。
  • sprintf - 当无法用 getField 替代时,应使用 sprintf。例如,select('IDENTITY(%s.%s) as %s', $alias, $fieldName, $fieldAlias)
  • checkIdentifier - 应用于检查标识符(字母数字字符串)。传递给 checkIdentifier 的变量被认为是安全的,并允许进一步使用。
  • checkField - 与 checkIdentifier 类似,但设计用于检查格式为 "\w+.\w+"(alias.fieldName)的字符串。传递给 checkField 的变量被认为是安全的,并允许进一步使用。
  • checkParameter - 与 checkIdentifier 类似,但设计用于检查格式为 ":\w+"(:parameterName)的字符串。传递给 checkParameter 的变量被认为是安全的,并允许进一步使用。
  • getSortOrder - 如果传递了 ASC 或 DESC 中的一个值,则返回 ASC 或 DESC。否则,抛出异常。用于清除作为参数传递的排序方向。

注意!!! ->select(sprintf(%s as something', $fullName)) 可能不会迅速修复,因为 $fullName 可能包含 CONCAT(firstName, lastName) 或其他任何语句。这样的调用应进行检查并标记为安全

DBAL

使用绑定参数或使用连接的引号方法引用它们。标识符应使用 QueryBuilderUtil 进行安全检查或使用连接的 quoteIdentifier 方法引用。

常见警告及其可能的修复方法
  • 不安全的字段用作查询的一部分

    $queryBuilder->andWhere($queryBuilder->expr()->eq($field, ':parameter'));

    修复 - 使用 QueryBuilderUtil::checkField 检查字段的安全性

    QueryBuilderUtil::checkField($field);
    $queryBuilder->andWhere($queryBuilder->expr()->eq($field, ':parameter'));
  • 使用复合标识符

    $queryBuilder->andWhere($queryBuilder->expr()->eq($alias . '.' . $field, ':parameter'));

    $queryBuilder->andWhere($queryBuilder->expr()->eq(sprintf('%s.%s', $alias, $field), ':parameter'));

    可能的修复方法。

    修复 1 - 使用 QueryBuilderUtil::getField

    $queryBuilder->andWhere($queryBuilder->expr()->eq(QueryBuilderUtil::getField($alias, $field), ':parameter'));

    修复 2 - 使用 QueryBuilderUtil::checkIdentifier 分别检查每个标识符

    QueryBuilderUtil::checkIdentifier($alias);
    QueryBuilderUtil::checkIdentifier($field);
    $queryBuilder->andWhere($queryBuilder->expr()->eq($alias . '.' . $field, ':parameter'));

    修复 3 - 使用安全的 QueryBuilderUtil::sprintf,也适用于替换 sprintf

    $queryBuilder->andWhere($queryBuilder->expr()->eq(QueryBuilderUtil::sprintf('%s.%s', $alias, $field), ':parameter'));
  • 使用复合参数名

    $queryBuilder->andWhere($queryBuilder->expr()->eq('table.id', $paramer));

    修复 - 使用 QueryBuilderUtil::checkParameter

    QueryBuilderUtil::checkParameter($paramer);
    $queryBuilder->andWhere($queryBuilder->expr()->eq('table.id', $paramer));
  • 使用从外部传递的排序顺序

    $queryBuilder->orderBy('table.field', $sortOrder);

    修复 - 使用 QueryBuilderUtil::getSortOrder

    $queryBuilder->orderBy('table.field', QueryBuilderUtil::getSortOrder($sortOrder));
  • 将文字传递到查询中

    $queryBuilder->select(sprintf("'%s' as className", $className));

    修复 - 使用 literal 表达式

    $queryBuilder->select(
      sprintf((string)$queryBuilder->expr()->literal($className) . ' as className')
    );

静态代码分析 - 配置

如果变量、属性或方法在详细的手动分析后被认为是安全的,则可以添加到 trusted_data.neon。这些项目将在进一步的检查中被标记为安全并跳过。

可用的 trusted_data.neon 配置部分有

  • variables - 安全变量的白名单。格式 class.method.variable: true
  • properties - 安全属性的白名单。格式 class.method.property: true
  • safe_methods - 安全类方法的白名单。格式 class.method: true
  • safe_static_methods - 安全类静态方法的白名单。格式 class.method: true
  • check_methods_safety - 如果传递的变量是安全的,则认为方法安全。格式 class.method: true 当所有传递的变量都应该检查时。包括 __construct 方法用于新实例创建检查
    class.method: [1] 当只需要检查某些变量(它们的索引在数组中列出)时
  • check_static_methods_safety - 如果传递的变量是安全的,则认为静态方法安全。格式 class.method: true 当所有传递的变量都应该检查时或 class.method: [1] 当只需要检查某些变量(它们的索引在数组中列出)时
  • clear_methods - 如果变量作为参数传递到列出的方法中,则被认为是安全的。格式 class.method: true
  • clear_static_methods - 如果变量作为参数传递到列出的静态方法中,则被认为是安全的。格式 class.method: true
  • check_methods - 包含一个用于检查安全性的方法列表。如果传入的参数不安全,分析工具会报告有关此类使用的安全警告。格式为 class.method: true 当所有传入的变量都应进行检查时,或者 class.method: [1] 当只有某些变量需要检查(其位置在数组中列出)。使用 class.__all__: true 来检查所有类方法。例如,有 SomeClass,我们想检查其所有方法,除了 method1。对于 method1,我们只想启用第一个和第三个参数的检查,而对于 method2,我们想检查所有参数
    check_methods:
        SomeClass:
            __all__: true
            method1: [0, 2]
            mrthod2: true

建议将方法标记为安全。如果一个变量由多个部分组成,最好是向白名单添加最小的非安全部分,而不是整个表达式。

示例

protected function addWhereToQueryBuilder(QueryBuilder $qb, string $suffix, int $index)
{
    $rootAlias = $qb->getRootAlias();
    $fieldName = $rootAlias . '.field' . $idx . $suffix;

    $qb->andWhere($qb->expr()->gt($fieldName, 10);
}

这样的代码会导致安全警告,因为 $fieldName 变量是由多个部分构成的,其中一些部分是不安全的。使此表达式安全的最佳解决方案是使用 QueryBuilderUtil::checkIdentifier($suffix) 检查 $suffix。另一种选择是将 $suffix 添加到 trusted_data.neon 白名单中,如果其值总是作为安全值传递或由调用者检查。最差的解决方案是将 $fieldName 标记为安全,因为其部分可能会改变,在添加新的或非安全部分后,它将被忽略,尽管它可能包含未检查的漏洞。

贡献

有关如何为此包和其他 Oro 产品做出贡献的信息,请参阅 Oro 社区指南