oro / phpstan-rules
一套用于Oro产品的额外PHPStan规则。
Requires
- nette/neon: ^3.1
- phpstan/phpstan: 1.8.*
- phpstan/phpstan-doctrine: 1.3.*
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 社区指南。