├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── doc └── QueryBuilderInjectionRule.md ├── extension.neon ├── phpstan.neon ├── rules.neon └── src └── Oro ├── Extension └── RemoveDefaultRulesExtension.php ├── Rules ├── Math │ ├── BaseMathOperationsRule.php │ ├── MathTypeOperationsRule.php │ ├── ProhibitBrickMathOperationsRule.php │ ├── ProhibitOroMathOperationsRule.php │ └── UnsafeMathOperationsRule.php ├── Methods │ └── QueryBuilderInjectionRule.php ├── Types │ ├── BrickMathReturnTypeExtension.php │ ├── DoctrineConnectionReturnTypeExtension.php │ ├── EntityManagerReturnTypeExtension.php │ ├── GetRepositoryDynamicReturnTypeExtension.php │ ├── ManagerRegistryEMReturnTypeExtension.php │ ├── MathTypes │ │ ├── BrickMathFloatType.php │ │ ├── BrickMathIntegerType.php │ │ ├── BrickMathStringType.php │ │ ├── OroMathFloatType.php │ │ ├── OroMathIntegerType.php │ │ └── OroMathStringType.php │ ├── ObjectRepositoryReturnTypeExtension.php │ ├── OroMathComponentReturnTypeExtension.php │ └── RequestGetSessionTypeExtension.php └── ValidExceptionCatchRule.php └── TrustedDataConfigurationFinder.php /.gitignore: -------------------------------------------------------------------------------- 1 | /logs 2 | /.settings 3 | /.buildpath 4 | /.project 5 | /.idea 6 | /composer.phar 7 | composer.lock 8 | /vendor 9 | phpunit.xml 10 | *~ 11 | /components 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oro, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oro's rules for PHPStan 2 | 3 | This package contains a set of additional rules for [PHPStan - PHP Static Analysis Tool](https://github.com/phpstan/phpstan). 4 | 5 | We use these rules at Oro, Inc. and ask anyone contributing code to Oro Products to follow them as well. 6 | 7 | ## Rules 8 | 9 | ### Unsafe DQL usage analysis 10 | 11 | #### Why DQL and SQL queries should be checked 12 | Using DQL does not protect against injection vulnerabilities. The following APIs are designed to be SAFE from SQL injections: 13 | - For Doctrine\DBAL\Connection#insert($table, $values, $types), Doctrine\DBAL\Connection#update($table, $values, $where, $types) and Doctrine\DBAL\Connection#delete($table, $where, $types) only the array values of $values and $where. The table name and keys of $values and $where are NOT escaped. 14 | - Doctrine\DBAL\Query\QueryBuilder#setFirstResult($offset) 15 | - Doctrine\DBAL\Query\QueryBuilder#setMaxResults($limit) 16 | - Doctrine\DBAL\Platforms\AbstractPlatform#modifyLimitQuery($sql, $limit, $offset) for the $limit and $offset parameters. 17 | 18 | Consider ALL other APIs to be not safe for user-input: 19 | 20 | - Query methods on the Connection 21 | - The QueryBuilder API 22 | - The Platforms and SchemaManager APIs to generate and execute DML/DDL SQL statements 23 | - Expressions constructed with the help of various Expression Builders 24 | 25 | See full article at [Doctrine Security](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/security.html) 26 | 27 | #### Static code analysis - Execution 28 | As checking the whole codebase requires a lot of time, sql-injection search tool was added to simplify the process. 29 | The tool is based on [PHPStan - PHP Static Analysis Tool](https://github.com/phpstan/phpstan) 30 | and is implemented as additional Rule. 31 | 32 | To check codebase for unsafe DQL and SQL usages perform the following actions: 33 | - change directory to `/tool/` where `` is path to application in the file system 34 | - install dependencies `composer install` 35 | - run check with `./bin/phpstan analyze -c phpstan.neon --autoload-file=` 36 | 37 | To speedup analysis it's recommended to run it in parallel on per package basis. This may be achieved with the help of the `parallel` command: 38 | ``` 39 | cd my_application/tool/ 40 | COMPOSER=composer-sql.json composer install 41 | rm -rf logs; 42 | mkdir logs; 43 | ls ../package/ \ 44 | | grep -v "\-demo" | grep -v "demo-" | grep -v "test-" | grep -v "german-" | grep -v "view-switcher" | grep -v "twig-inspector" \ 45 | | parallel -j 4 "./bin/phpstan analyze -c phpstan.neon `pwd`/../package/{} --autoload-file=`pwd`/../application/commerce-crm-ee/vendor/autoload.php > logs/{}.log" 46 | ``` 47 | Note that _commerce-crm-ee_ application should have `autoload.php` generated. 48 | The results of the analysis should be available within a minute. Each result should be checked carefully. Unsafe variables should be sanitized or escaped as a precaution. 49 | 50 | #### HOW TO fix found warnings 51 | Unsafe variables are any methods that may depend on external data. Even cached data may be unsafe if an attacker manages to access the cache storage. 52 | Any variable that comes from the outside, or contains a value returned by an unsafe function, is also unsafe and should be passed into queries with caution. 53 | 54 | You can make a variable safe in a number of ways: 55 | 56 | ##### ORM 57 | ORM based queries may contain vulnerable inputs. To keep them clean, follow the next rules: 58 | 59 | - Parameter identifier *MUST* be a named placeholder. Passing data directly as the right operand is prohibited. 60 | The only possible exception is numbers, `\DateTime` and booleans. 61 | - All identifiers MUST BE [a-zA-Z0-9_] compatible. 62 | 63 | If there is a need to pass a variable directly into the query, use `QueryBuilderUtil` safe methods 64 | - *getField* - use it when field is constructed with sprintf or concatenation. For example, `select($alias . '.' . $field)`, `select('alias.', $field)`, `select(sprintf('%s.%s', $alias, $field)` , etc. 65 | - *sprintf* - should be used instead of sprintf when it cannot be replaced with getField. For example, `select('IDENTITY(%s.%s) as %s', $alias, $fieldName, $fieldAlias)` 66 | - *checkIdentifier* - should be used to check identifiers (alphanumeric strings). A variable passed to checkIdentifier is considered safe and is allowed for further use. 67 | - *checkField* - similar to checkIdentifier with exception that it's designed to check strings in format "\w+.\w+" (alias.fieldName). A variable passed to checkField is considered safe and is allowed for further use. 68 | - *checkParameter* -similar to checkIdentifier with exception that it's designed to check strings in format ":\w+" (:parameterName). A variable passed to checkParameter is considered safe and is allowed for further use. 69 | - *getSortOrder* - return ASC or DESC if one of these values is passed. Otherwise, an exception is thrown. Used to clear sort directions passed as parameters. 70 | 71 | > NOTE!!! ->select(sprintf(%s as something', $fullName)) may be not quick fixed as $fullName may contain CONCAT(firstName, lastName) or any other statement. Such calls should be checked and marked safe 72 | 73 | ##### DBAL 74 | Use bind parameters or quote them with the connection quote method. 75 | Identifiers should be either checked for safety with QueryBuilderUtil or quoted with the quoteIdentifier method of connection. 76 | 77 | ##### Common warnings and possible ways to fix them 78 | 79 | - Unsafe field is used as a part of query 80 | 81 | ```php 82 | $queryBuilder->andWhere($queryBuilder->expr()->eq($field, ':parameter')); 83 | ``` 84 | 85 | Fix - use `QueryBuilderUtil::checkField` to check field for safeness 86 | ```php 87 | QueryBuilderUtil::checkField($field); 88 | $queryBuilder->andWhere($queryBuilder->expr()->eq($field, ':parameter')); 89 | ``` 90 | 91 | - Using composite identifier 92 | ```php 93 | $queryBuilder->andWhere($queryBuilder->expr()->eq($alias . '.' . $field, ':parameter')); 94 | ``` 95 | 96 | Or 97 | 98 | ```php 99 | $queryBuilder->andWhere($queryBuilder->expr()->eq(sprintf('%s.%s', $alias, $field), ':parameter')); 100 | ``` 101 | 102 | Possible ways to fix. 103 | 104 | Fix 1 - use `QueryBuilderUtil::getField` 105 | ```php 106 | $queryBuilder->andWhere($queryBuilder->expr()->eq(QueryBuilderUtil::getField($alias, $field), ':parameter')); 107 | ``` 108 | 109 | Fix 2 - check each identifier separately with `QueryBuilderUtil::checkIdentifier` 110 | ```php 111 | QueryBuilderUtil::checkIdentifier($alias); 112 | QueryBuilderUtil::checkIdentifier($field); 113 | $queryBuilder->andWhere($queryBuilder->expr()->eq($alias . '.' . $field, ':parameter')); 114 | ``` 115 | 116 | Fix 3 - use safe `QueryBuilderUtil::sprintf`, also applicable when replacing sprintf 117 | ```php 118 | $queryBuilder->andWhere($queryBuilder->expr()->eq(QueryBuilderUtil::sprintf('%s.%s', $alias, $field), ':parameter')); 119 | ``` 120 | 121 | - Using composite parameter name 122 | ```php 123 | $queryBuilder->andWhere($queryBuilder->expr()->eq('table.id', $paramer)); 124 | ``` 125 | 126 | Fix - use `QueryBuilderUtil::checkParameter` 127 | 128 | ```php 129 | QueryBuilderUtil::checkParameter($paramer); 130 | $queryBuilder->andWhere($queryBuilder->expr()->eq('table.id', $paramer)); 131 | ``` 132 | - Using sort order passed from outside 133 | 134 | ```php 135 | $queryBuilder->orderBy('table.field', $sortOrder); 136 | ``` 137 | 138 | Fix - use `QueryBuilderUtil::getSortOrder` 139 | 140 | ```php 141 | $queryBuilder->orderBy('table.field', QueryBuilderUtil::getSortOrder($sortOrder)); 142 | ``` 143 | 144 | - Literal is passed to query 145 | 146 | ```php 147 | $queryBuilder->select(sprintf("'%s' as className", $className)); 148 | ``` 149 | 150 | Fix use `literal` expression 151 | 152 | ```php 153 | $queryBuilder->select( 154 | sprintf((string)$queryBuilder->expr()->literal($className) . ' as className') 155 | ); 156 | ``` 157 | 158 | #### Static code analysis - Configuration 159 | If a variable, a property or a method are considered safe after a detailed manual analysis, they may be added to `trusted_data.neon`. 160 | Such items will be marked as safe during further checks and skipped. 161 | 162 | Available `trusted_data.neon` configuration sections are: 163 | - `variables` - whitelist of safe variables. Format `class.method.variable: true` 164 | - `properties` - whitelist of safe properties. Format `class.method.property: true` 165 | - `safe_methods` - whitelist of safe class methods. Format `class.method: true` 166 | - `safe_static_methods` - whitelist of safe class static methods. Format `class.method: true` 167 | - `check_methods_safety` - consider method safe if passed variables are safe. Format `class.method: true` when all passed variables should be checked. Includes `__construct` method for new instance creation checks 168 | or `class.method: [1]` when only certain variables require checks (their positions are listed in array) 169 | - `check_static_methods_safety` - consider static method safe if passed variables are safe. Format `class.method: true` when all passed variables should be checked 170 | or `class.method: [1]` when only certain variables require checks (their positions are listed in array) 171 | - `clear_methods` - variable is considered as safe if it is passed as argument into listed method. Format `class.method: true` 172 | - `clear_static_methods` - variable is considered as safe if it is passed as argument into listed static method. Format `class.method: true` 173 | - `check_methods` - contains a list of methods that are checked for safeness. If passed arguments are unsafe, a security warning about such usage is reported by the analysis tool. 174 | Format `class.method: true` when all passed variables should be checked or `class.method: [1]` when only certain variables require checks (their positions are listed in array). 175 | Use `class.__all__: true` to check all class methods. 176 | For example, there is SomeClass and we want to check all its methods, except for `method1`. For `method1`, 177 | we want to enable only the first and third argument checks, and for `method2` we want all arguments to be checked: 178 | ```yml 179 | check_methods: 180 | SomeClass: 181 | __all__: true 182 | method1: [0, 2] 183 | mrthod2: true 184 | ``` 185 | 186 | It is recommended to mark methods as safe. If a variable consists of several parts, it is better to add a minimal unsafe part to the whitelist, rather than the whole expression. 187 | 188 | #### Example 189 | ```php 190 | protected function addWhereToQueryBuilder(QueryBuilder $qb, string $suffix, int $index) 191 | { 192 | $rootAlias = $qb->getRootAlias(); 193 | $fieldName = $rootAlias . '.field' . $idx . $suffix; 194 | 195 | $qb->andWhere($qb->expr()->gt($fieldName, 10); 196 | } 197 | ``` 198 | 199 | Such code will lead to a security warning, as `$fieldName` variable was constructed using several parts, some of which are not safe. 200 | The best solution to make this expression safe is to check `$suffix` with `QueryBuilderUtil::checkIdentifier($suffix)` 201 | Another option is to add `$suffix` into the `trusted_data.neon` whitelist if its values are always passed as safe or checked in the caller. 202 | The worst solution would be to mark `$fieldName` as safe because its parts may be changed and, after adding a new or an unsafe part, it will be skipped, although it may contain an unchecked vulnerability. 203 | 204 | 205 | ## Contribute 206 | 207 | Please referer to [Oro Community Guide](https://oroinc.com/orocommerce/doc/current/community/contribute) for information on how to contribute to this package and other Oro products. 208 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oro/phpstan-rules", 3 | "description": "A set of additional PHPStan rules used in Oro products.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Oro, Inc", 8 | "homepage": "https://oroinc.com" 9 | } 10 | ], 11 | "minimum-stability": "dev", 12 | "prefer-stable": true, 13 | "extra": { 14 | "branch-alias": { 15 | "dev-master": "1.12.x-dev" 16 | }, 17 | "phpstan": { 18 | "includes": [ 19 | "phpstan.neon", 20 | "extension.neon", 21 | "rules.neon" 22 | ] 23 | } 24 | }, 25 | "autoload": { 26 | "psr-4": {"": "src/"} 27 | }, 28 | "require": { 29 | "phpstan/phpstan": "1.12.*", 30 | "phpstan/phpstan-doctrine": "1.5.*", 31 | "nette/neon": "^3.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /doc/QueryBuilderInjectionRule.md: -------------------------------------------------------------------------------- 1 | # QueryBuilderInjectionRule 2 | 3 | This rule analyzes source code for unsafe use of SQL and DQL queries. 4 | 5 | ## Why DQL and SQL queries should be checked 6 | 7 | Using DQL does not protect against injection vulnerabilities. 8 | 9 | See the full article at [Doctrine Security](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/security.html) 10 | 11 | The following APIs are designed to be SAFE from SQL injections: 12 | - For Doctrine\DBAL\Connection#insert($table, $values, $types), Doctrine\DBAL\Connection#update($table, $values, $where, $types) and Doctrine\DBAL\Connection#delete($table, $where, $types) only the array values of $values and $where. The table name and keys of $values and $where are NOT escaped. 13 | - Doctrine\DBAL\Query\QueryBuilder#setFirstResult($offset) 14 | - Doctrine\DBAL\Query\QueryBuilder#setMaxResults($limit) 15 | - Doctrine\DBAL\Platforms\AbstractPlatform#modifyLimitQuery($sql, $limit, $offset) for the $limit and $offset parameters. 16 | 17 | Consider ALL other APIs to be not safe for user-input: 18 | 19 | - Query methods on the Connection 20 | - The QueryBuilder API 21 | - The Platforms and SchemaManager APIs to generate and execute DML/DDL SQL statements 22 | - Expressions constructed with the help of various Expression Builders 23 | 24 | 25 | ## How to fix the issues 26 | 27 | Unsafe variables are produced by any methods that may depend on external data. Even cached data may be unsafe if an attacker manages to pollute the cache storage. 28 | Any variable that comes from the outside, or contains a value returned by an unsafe function, is also unsafe and should be passed into queries with caution. 29 | 30 | You can make a variable safe in a number of ways: 31 | 32 | ### ORM 33 | 34 | ORM-based queries may contain vulnerable inputs. To keep them clean, follow the next rules: 35 | 36 | - Parameter identifier *MUST* be a named placeholder. Passing data directly as the right operand is prohibited. The only possible exceptions are numbers, `\DateTime` and booleans. 37 | - All identifiers MUST BE [a-zA-Z0-9_] compatible. 38 | 39 | If there is a need to pass a variable directly into the query, use `QueryBuilderUtil` safe methods 40 | - *getField* - use it when a field is constructed with sprintf or concatenation. For example, `select($alias . '.' . $field)`, `select('alias.', $field)`, `select(sprintf('%s.%s', $alias, $field)` , etc. 41 | - *sprintf* - should be used instead of sprintf when it cannot be replaced with getField. For example, `select('IDENTITY(%s.%s) as %s', $alias, $fieldName, $fieldAlias)` 42 | - *checkIdentifier* - should be used to check identifiers (alphanumeric strings). A variable passed to checkIdentifier is considered safe and is allowed for further use. 43 | - *checkField* - similar to checkIdentifier, but designed to check strings formatted as "\w+.\w+" (alias.fieldName). A variable passed to checkField is considered safe and is allowed for further use. 44 | - *checkParameter* -similar to checkIdentifier, but designed to check strings formatted as ":\w+" (:parameterName). A variable passed to checkParameter is considered safe and is allowed for further use. 45 | - *getSortOrder* - returns ASC or DESC if one of these values is passed. Otherwise, an exception will be thrown. Use to clear sort directions passed as parameters. 46 | 47 | > NOTE!!! ->select(sprintf(%s as something', $fullName)) may be not easily fixable if $fullName contains CONCAT(firstName, lastName) or some other statement. Such calls should be checked and may be marked safe. 48 | 49 | ### DBAL 50 | 51 | Use parameter binding or quote them with the connection quote method. Identifiers should be either checked for safety with QueryBuilderUtil or quoted with the quoteIdentifier method of connection. 52 | 53 | ### Common warnings and possible ways to fix them 54 | 55 | - Unsafe field is used as a part of query: 56 | 57 | ```php 58 | $queryBuilder->andWhere($queryBuilder->expr()->eq($field, ':parameter')); 59 | ``` 60 | 61 | Fix - use `QueryBuilderUtil::checkField` to check the field for safeness: 62 | 63 | ```php 64 | QueryBuilderUtil::checkField($field); 65 | $queryBuilder->andWhere($queryBuilder->expr()->eq($field, ':parameter')); 66 | ``` 67 | 68 | - Using composite identifier: 69 | 70 | ```php 71 | $queryBuilder->andWhere($queryBuilder->expr()->eq($alias . '.' . $field, ':parameter')); 72 | ``` 73 | 74 | Or: 75 | 76 | ```php 77 | $queryBuilder->andWhere($queryBuilder->expr()->eq(sprintf('%s.%s', $alias, $field), ':parameter')); 78 | ``` 79 | 80 | Possible fix - use `QueryBuilderUtil::getField` 81 | ```php 82 | $queryBuilder->andWhere($queryBuilder->expr()->eq(QueryBuilderUtil::getField($alias, $field), ':parameter')); 83 | ``` 84 | 85 | Fix 2 - check each identifier separately with `QueryBuilderUtil::checkIdentifier` 86 | ```php 87 | QueryBuilderUtil::checkIdentifier($alias); 88 | QueryBuilderUtil::checkIdentifier($field); 89 | $queryBuilder->andWhere($queryBuilder->expr()->eq($alias . '.' . $field, ':parameter')); 90 | ``` 91 | 92 | Fix 3 - use safe `QueryBuilderUtil::sprintf`, also applicable when replacing sprintf 93 | ```php 94 | $queryBuilder->andWhere($queryBuilder->expr()->eq(QueryBuilderUtil::sprintf('%s.%s', $alias, $field), ':parameter')); 95 | ``` 96 | 97 | - Using composite parameter name 98 | ```php 99 | $queryBuilder->andWhere($queryBuilder->expr()->eq('table.id', $paramer)); 100 | ``` 101 | 102 | Fix - use `QueryBuilderUtil::checkParameter` 103 | 104 | ```php 105 | QueryBuilderUtil::checkParameter($paramer); 106 | $queryBuilder->andWhere($queryBuilder->expr()->eq('table.id', $paramer)); 107 | ``` 108 | - Using sort order passed from outside 109 | 110 | ```php 111 | $queryBuilder->orderBy('table.field', $sortOrder); 112 | ``` 113 | 114 | Fix - use `QueryBuilderUtil::getSortOrder` 115 | 116 | ```php 117 | $queryBuilder->orderBy('table.field', QueryBuilderUtil::getSortOrder($sortOrder)); 118 | ``` 119 | 120 | - Literal is passed to query 121 | 122 | ```php 123 | $queryBuilder->select(sprintf("'%s' as className", $className)); 124 | ``` 125 | 126 | Fix use `literal` expression 127 | 128 | ```php 129 | $queryBuilder->select( 130 | sprintf((string)$queryBuilder->expr()->literal($className) . ' as className') 131 | ); 132 | ``` 133 | 134 | ## Static code analysis - Configuration 135 | Trusted Data is organized per bundle, each separate config file is placed in `Tests/trusted_data.neon` and loaded automatically. 136 | Configuration tree in `Tests/trusted_data.neon` files is organized under `trusted_data` root. 137 | Trusted data may be also configured via DI as `sql_injection_testing.trusted_data`. 138 | 139 | If a variable, a property or a method are considered safe after a detailed manual analysis, they may be added to `Tests/trusted_data.neon`. 140 | Such items will be marked as safe during further checks and skipped. 141 | 142 | Available Trusted Data configuration sections are: 143 | - `variables` - whitelist of safe variables. Format `class.method.variable: true` 144 | - `properties` - whitelist of safe properties. Format `class.method.property: true` 145 | - `safe_methods` - whitelist of safe class methods. Format `class.method: true` 146 | - `safe_static_methods` - whitelist of safe class static methods. Format `class.method: true` 147 | - `check_methods_safety` - consider method safe if passed variables are safe. Format `class.method: true` when all passed variables should be checked 148 | or `class.method: [1]` when only certain variables require checks (their positions are listed in array) 149 | - `check_static_methods_safety` - consider static method safe if passed variables are safe. Format `class.method: true` when all passed variables should be checked 150 | or `class.method: [1]` when only certain variables require checks (their positions are listed in array) 151 | - `clear_methods` - variable is considered as safe if it is passed as argument into listed method. Format `class.method: true` 152 | - `clear_static_methods` - variable is considered as safe if it is passed as argument into listed static method. Format `class.method: true` 153 | - `check_methods` - contains a list of methods that are checked for safeness. If passed arguments are unsafe, a security warning about such usage is reported by the analysis tool. 154 | Format `class.method: true` when all passed variables should be checked or `class.method: [1]` when only certain variables require checks (their positions are listed in array). 155 | Use `class.__all__: true` to check all class methods. 156 | For example, there is SomeClass and we want to check all its methods, except for `method1`. For `method1`, 157 | we want to enable only the first and third argument checks, and for `method2` we want all arguments to be checked: 158 | ```yml 159 | check_methods: 160 | SomeClass: 161 | __all__: true 162 | method1: [0, 2] 163 | mrthod2: true 164 | ``` 165 | 166 | It is recommended to mark methods as safe. If a variable consists of several parts, it is better to add a minimal unsafe part to the whitelist, rather than the whole expression. 167 | 168 | ## Example 169 | ```php 170 | protected function addWhereToQueryBuilder(QueryBuilder $qb, string $suffix, int $index) 171 | { 172 | $rootAlias = $qb->getRootAlias(); 173 | $fieldName = $rootAlias . '.field' . $idx . $suffix; 174 | 175 | $qb->andWhere($qb->expr()->gt($fieldName, 10); 176 | } 177 | ``` 178 | 179 | Such code will lead to a security warning, as `$fieldName` variable was constructed using several parts, some of which are not safe. 180 | The best solution to make this expression safe is to check `$suffix` with `QueryBuilderUtil::checkIdentifier($suffix)` 181 | Another option is to add `$suffix` into the `trusted_data.neon` whitelist if its values are always passed as safe or checked in the caller. 182 | The worst solution would be to mark `$fieldName` as safe because its parts may be changed and, after adding a new or an unsafe part, it will be skipped, although it may contain an unchecked vulnerability. 183 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - 3 | class: Oro\Rules\Methods\QueryBuilderInjectionRule 4 | arguments: 5 | checkThisOnly: %checkThisOnly% 6 | trustedData: %sql_injection_testing.trusted_data% 7 | tags: 8 | - phpstan.rules.rule 9 | 10 | - 11 | class: Oro\Rules\Types\DoctrineConnectionReturnTypeExtension 12 | tags: 13 | - phpstan.broker.dynamicMethodReturnTypeExtension 14 | 15 | - 16 | class: Oro\Rules\Types\RequestGetSessionTypeExtension 17 | tags: 18 | - phpstan.broker.dynamicMethodReturnTypeExtension 19 | 20 | - 21 | class: Oro\Rules\Types\EntityManagerReturnTypeExtension 22 | arguments: 23 | supportedClass: Doctrine\ORM\EntityManagerInterface 24 | tags: 25 | - phpstan.broker.dynamicMethodReturnTypeExtension 26 | 27 | - 28 | class: Oro\Rules\Types\EntityManagerReturnTypeExtension 29 | arguments: 30 | supportedClass: Doctrine\Persistence\ManagerRegistry 31 | tags: 32 | - phpstan.broker.dynamicMethodReturnTypeExtension 33 | 34 | - 35 | class: Oro\Rules\Types\ManagerRegistryEMReturnTypeExtension 36 | arguments: 37 | supportedClass: Doctrine\Persistence\ManagerRegistry 38 | tags: 39 | - phpstan.broker.dynamicMethodReturnTypeExtension 40 | 41 | - 42 | class: Oro\Rules\Types\ObjectRepositoryReturnTypeExtension 43 | arguments: 44 | supportedClass: Doctrine\ORM\EntityRepository 45 | tags: 46 | - phpstan.broker.dynamicMethodReturnTypeExtension 47 | 48 | persistenceManagerRegistryGetRepository: 49 | class: Oro\Rules\Types\GetRepositoryDynamicReturnTypeExtension 50 | tags: 51 | - phpstan.broker.dynamicMethodReturnTypeExtension 52 | arguments: 53 | managerClass: Doctrine\Persistence\ManagerRegistry 54 | 55 | persistenceObjectManagerGetRepository: 56 | class: Oro\Rules\Types\GetRepositoryDynamicReturnTypeExtension 57 | tags: 58 | - phpstan.broker.dynamicMethodReturnTypeExtension 59 | arguments: 60 | managerClass: Doctrine\Persistence\ObjectManager 61 | 62 | managerRegistryGetRepository: 63 | class: Oro\Rules\Types\GetRepositoryDynamicReturnTypeExtension 64 | tags: 65 | - phpstan.broker.dynamicMethodReturnTypeExtension 66 | arguments: 67 | managerClass: Doctrine\Persistence\ManagerRegistry 68 | 69 | objectManagerGetRepository: 70 | class: Oro\Rules\Types\GetRepositoryDynamicReturnTypeExtension 71 | tags: 72 | - phpstan.broker.dynamicMethodReturnTypeExtension 73 | arguments: 74 | managerClass: Doctrine\Persistence\ObjectManager 75 | 76 | # Should be uncommented after existing problems fixed 77 | #- 78 | #class: Oro\Rules\ValidExceptionCatchRule 79 | #tags: 80 | #- phpstan.rules.rule 81 | 82 | parametersSchema: 83 | sql_injection_testing: structure([ 84 | trusted_data: structure([ 85 | variables: arrayOf(arrayOf(arrayOf(bool()))) 86 | properties: arrayOf(arrayOf(arrayOf(bool()))) 87 | safe_static_methods: arrayOf(arrayOf(bool())) 88 | safe_methods: arrayOf(arrayOf(anyOf(bool(), arrayOf(int())))) 89 | check_static_methods_safety: arrayOf(arrayOf(anyOf(bool(), arrayOf(int())))) 90 | check_methods_safety: arrayOf(arrayOf(anyOf(bool(), arrayOf(int())))) 91 | check_methods: arrayOf(arrayOf(anyOf(bool(), arrayOf(anyOf(int(), string()))))) 92 | clear_static_methods: arrayOf(arrayOf(bool())) 93 | clear_methods: arrayOf(arrayOf(bool())) 94 | ]) 95 | ]) 96 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | customRulesetUsed: true 3 | checkThisOnly: false 4 | inferPrivatePropertyTypeFromConstructor: true 5 | -------------------------------------------------------------------------------- /rules.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | sql_injection_testing: 3 | trusted_data: 4 | variables: [] 5 | properties: [] 6 | safe_static_methods: [] 7 | 8 | safe_methods: 9 | Doctrine\DBAL\Connection: 10 | quote: true 11 | quoteIdentifier: true 12 | 13 | Doctrine\DBAL\Driver\Connection: 14 | quote: true 15 | 16 | Doctrine\ORM\Query\Expr\Join: 17 | getJoin: true 18 | getAlias: true 19 | getConditionType: true 20 | getIndexBy: true 21 | getCondition: true 22 | 23 | Doctrine\ORM\QueryBuilder: 24 | setParameter: true 25 | setParameters: true 26 | setMaxResults: true 27 | setFirstResult: true 28 | addCriteria: true 29 | getAllAliases: true 30 | getRootAliases: true 31 | getRootAlias: true 32 | getRootEntities: true 33 | getMaxResults: true 34 | getFirstResult: true 35 | getParameter: true 36 | getDQL: true 37 | getQuery: true 38 | 39 | Doctrine\ORM\Query: 40 | getDQL: true 41 | getSQL: true 42 | 43 | Doctrine\DBAL\Query\QueryBuilder: 44 | setParameter: true 45 | setParameters: true 46 | setMaxResults: true 47 | setFirstResult: true 48 | getAllAliases: true 49 | getRootAliases: true 50 | getRootAlias: true 51 | getRootEntities: true 52 | getMaxResults: true 53 | getFirstResult: true 54 | getParameter: true 55 | getQuery: true 56 | 57 | Doctrine\ORM\Mapping\ClassMetadata: 58 | getSingleIdentifierFieldName: true 59 | getIdentifierFieldNames: true 60 | getIdentifier: true 61 | getTableName: true 62 | getAssociationMapping: true 63 | getColumnNames: true 64 | 65 | Doctrine\ORM\Mapping\ClassMetadataInfo: 66 | getSingleIdentifierFieldName: true 67 | getIdentifierFieldNames: true 68 | getIdentifier: true 69 | getTableName: true 70 | getAssociationMapping: true 71 | 72 | Doctrine\Persistence\Mapping\ClassMetadata: 73 | getIdentifier: true 74 | getIdentifierFieldNames: true 75 | getIdentifierValues: true 76 | 77 | Doctrine\ORM\Query\Expr: 78 | literal: true 79 | 80 | Doctrine\ORM\Query\Expr\Base: 81 | add: true 82 | 83 | Doctrine\Common\Collections\ArrayCollection: 84 | count: true 85 | 86 | DateTime: 87 | format: true 88 | 89 | Doctrine\Common\Collections\Criteria: 90 | getMaxResults: true 91 | getFirstResult: true 92 | 93 | Symfony\Component\Validator\Context\ExecutionContextInterface: 94 | getPropertyName: true 95 | getClassName: true 96 | 97 | check_static_methods_safety: [] 98 | 99 | check_methods_safety: 100 | Doctrine\Common\Inflector\Inflector: 101 | pluralize: true 102 | camelize: true 103 | 104 | Doctrine\Inflector\Inflector: 105 | pluralize: true 106 | camelize: true 107 | 108 | Doctrine\Common\Collections\ExpressionBuilder: 109 | notExists: [0] 110 | 111 | Doctrine\DBAL\Platforms\AbstractPlatform: 112 | getTruncateTableSQL: [0] 113 | 114 | check_methods: 115 | Doctrine\ORM\QueryBuilder: 116 | __all__: true 117 | where: [0, 1] 118 | orWhere: [0, 1] 119 | andWhere: [0, 1] 120 | having: [0, 1] 121 | orHaving: [0, 1] 122 | andHaving: [0, 1] 123 | join: [0, 1, 3] 124 | leftJoin: [0, 1, 3] 125 | innerJoin: [0, 1, 3] 126 | 127 | Doctrine\DBAL\Query\QueryBuilder: 128 | __all__: true 129 | where: [0, 1] 130 | orWhere: [0, 1] 131 | andWhere: [0, 1] 132 | having: [0, 1] 133 | orHaving: [0, 1] 134 | andHaving: [0, 1] 135 | join: [0, 1, 3] 136 | leftJoin: [0, 1, 3] 137 | innerJoin: [0, 1, 3] 138 | 139 | Doctrine\ORM\Query\Expr: 140 | __all__: true 141 | in: [0] 142 | notIn: [0] 143 | eq: [0, 1] 144 | neq: [0, 1] 145 | gt: [0, 1] 146 | lt: [0, 1] 147 | gte: [0, 1] 148 | lte: [0, 1] 149 | like: [0, 1] 150 | notLike: [0, 1] 151 | between: [1, 2] 152 | isMemberOf: [0, 1] 153 | 154 | Doctrine\ORM\Query\Expr\Andx: 155 | add: true 156 | addMultiple: true 157 | 158 | Doctrine\ORM\Query\Expr\Orx: 159 | add: true 160 | addMultiple: true 161 | 162 | Doctrine\ORM\Query\Expr\Select: 163 | add: true 164 | addMultiple: true 165 | 166 | Doctrine\ORM\Query\Expr\GroupBy: 167 | add: true 168 | addMultiple: true 169 | 170 | Doctrine\ORM\Query\Expr\Coalesce: 171 | add: true 172 | addMultiple: true 173 | 174 | Doctrine\DBAL\Query\Expression\ExpressionBuilder: 175 | __all__: true 176 | 177 | Doctrine\DBAL\Connection: 178 | fetchAssoc: [0] 179 | fetchAssociative: [0] 180 | fetchAllAssociative: [0] 181 | fetchNumeric: [0] 182 | fetchOne: [0] 183 | fetchArray: [0] 184 | fetchColumn: [0] 185 | fetchAll: [0] 186 | delete: [0, 1:keys] 187 | update: [0, 1:keys, 2:keys] 188 | insert: [0, 1:keys] 189 | prepare: true 190 | executeQuery: [0] 191 | executeCacheQuery: [0] 192 | executeStatement: [0] 193 | project: [0] 194 | query: [0, 1] 195 | executeUpdate: [0] 196 | exec: true 197 | 198 | Doctrine\ORM\EntityManager: 199 | createQuery: true 200 | createNativeQuery: true 201 | 202 | clear_static_methods: [] 203 | 204 | clear_methods: 205 | Doctrine\ORM\Mapping\ClassMetadata: 206 | getAssociationTargetClass: true 207 | getAssociationMapping: true 208 | getFieldMapping: true 209 | getSingleAssociationJoinColumnName: true 210 | getSingleAssociationReferencedJoinColumnName: true 211 | getFieldForColumn: true 212 | getColumnName: true 213 | 214 | Doctrine\Persistence\ManagerRegistry: 215 | getManagerForClass: true 216 | 217 | Doctrine\Bundle\DoctrineBundle\Registry: 218 | getManagerForClass: true 219 | 220 | Symfony\Bridge\Doctrine\RegistryInterface: 221 | getManagerForClass: true 222 | -------------------------------------------------------------------------------- /src/Oro/Extension/RemoveDefaultRulesExtension.php: -------------------------------------------------------------------------------- 1 | getContainerBuilder(); 22 | 23 | foreach ($builder->findByTag(LazyRegistry::RULE_TAG) as $serviceName => $_) { 24 | $definition = $builder->getDefinition($serviceName); 25 | 26 | if (!$this->isOroRule($definition->getType())) { 27 | $builder->removeDefinition($serviceName); 28 | } 29 | } 30 | } 31 | 32 | private function isOroRule(?string $className): bool 33 | { 34 | if (null === $className) { 35 | return false; 36 | } 37 | foreach (self::ALLOWED_NAMESPACES as $namespace) { 38 | if (str_starts_with($className, $namespace)) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Oro/Rules/Math/BaseMathOperationsRule.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | abstract class BaseMathOperationsRule implements Rule 17 | { 18 | public function getNodeType(): string 19 | { 20 | return Node::class; 21 | } 22 | 23 | /** 24 | * Checks if the node is a mathematical operation 25 | */ 26 | protected function isMathOperation(Node $node): bool 27 | { 28 | return $node instanceof Node\Expr\BinaryOp\Plus 29 | || $node instanceof Node\Expr\BinaryOp\Minus 30 | || $node instanceof Node\Expr\BinaryOp\Mul 31 | || $node instanceof Node\Expr\BinaryOp\Div 32 | || $node instanceof Node\Expr\BinaryOp\Mod 33 | || $node instanceof Node\Expr\BinaryOp\Pow; 34 | } 35 | 36 | /** 37 | * Checks if the node is a mathematical operation with assignment 38 | */ 39 | protected function isAssignOp(Node $node): bool 40 | { 41 | return $node instanceof Node\Expr\AssignOp\Plus 42 | || $node instanceof Node\Expr\AssignOp\Minus 43 | || $node instanceof Node\Expr\AssignOp\Mul 44 | || $node instanceof Node\Expr\AssignOp\Div 45 | || $node instanceof Node\Expr\AssignOp\Mod 46 | || $node instanceof Node\Expr\AssignOp\Pow; 47 | } 48 | 49 | /** 50 | * Checks if node is any supported math operation (binary or assignment) 51 | */ 52 | protected function isAnyMathOperation(Node $node): bool 53 | { 54 | return $this->isMathOperation($node) || $this->isAssignOp($node); 55 | } 56 | 57 | /** 58 | * Creates an error with the specified message 59 | */ 60 | protected function createError(string $message, ?string $identifier = null): array 61 | { 62 | $builder = RuleErrorBuilder::message($message); 63 | 64 | if ($identifier !== null) { 65 | $builder->identifier($identifier); 66 | } 67 | 68 | return [$builder->build()]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Oro/Rules/Math/MathTypeOperationsRule.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class MathTypeOperationsRule extends BaseMathOperationsRule 18 | { 19 | /** 20 | * @var array 21 | */ 22 | protected array $restrictedTypes = []; 23 | 24 | /** 25 | * Component names for the error message 26 | */ 27 | protected string $componentName = ''; 28 | protected string $alternativeMethod = ''; 29 | 30 | /** 31 | * Rule identifier 32 | */ 33 | protected string $identifier = 'prohibitedMathOperations.prohibitedOperation'; 34 | 35 | public function processNode(Node $node, Scope $scope): array 36 | { 37 | if (!$this->isAnyMathOperation($node)) { 38 | return []; 39 | } 40 | 41 | if ($this->isMathOperation($node)) { 42 | return $this->processBinaryOp($node, $scope); 43 | } 44 | 45 | if ($this->isAssignOp($node)) { 46 | return $this->processAssignOp($node, $scope); 47 | } 48 | 49 | return []; 50 | } 51 | 52 | /** 53 | * Process a binary operation node (like +, -, *, /) 54 | */ 55 | protected function processBinaryOp(Node\Expr\BinaryOp $node, Scope $scope): array 56 | { 57 | // Cast check 58 | foreach ([$node->left, $node->right] as $operand) { 59 | if ($operand instanceof Node\Expr\Cast) { 60 | $innerType = $scope->getType($operand->expr); 61 | if ($this->isRestrictedType($innerType)) { 62 | return $this->createError( 63 | sprintf( 64 | "Arithmetic operations on %s objects (even after casting) are prohibited. " . 65 | "Use %s methods instead.", 66 | $this->componentName, 67 | $this->alternativeMethod 68 | ), 69 | $this->identifier 70 | ); 71 | } 72 | } 73 | } 74 | 75 | // Regular type check 76 | $leftType = $scope->getType($node->left); 77 | $rightType = $scope->getType($node->right); 78 | 79 | foreach ([$leftType, $rightType] as $type) { 80 | if ($this->isRestrictedType($type)) { 81 | return $this->createError( 82 | sprintf( 83 | "Arithmetic operations on %s objects are prohibited. Use %s methods instead.", 84 | $this->componentName, 85 | $this->alternativeMethod 86 | ), 87 | $this->identifier 88 | ); 89 | } 90 | } 91 | 92 | return []; 93 | } 94 | 95 | /** 96 | * Process an assignment operation node (like +=, -=, *=) 97 | */ 98 | protected function processAssignOp(Node\Expr\AssignOp $node, Scope $scope): array 99 | { 100 | // Check var type 101 | $varType = $scope->getType($node->var); 102 | if ($this->isRestrictedType($varType)) { 103 | return $this->createError( 104 | sprintf( 105 | "Arithmetic assignment operations on %s objects are prohibited. Use %s methods instead.", 106 | $this->componentName, 107 | $this->alternativeMethod 108 | ), 109 | $this->identifier 110 | ); 111 | } 112 | 113 | // Check expr type 114 | $exprType = $scope->getType($node->expr); 115 | if ($this->isRestrictedType($exprType)) { 116 | return $this->createError( 117 | sprintf( 118 | "Arithmetic assignment operations with %s objects are prohibited. Use %s methods instead.", 119 | $this->componentName, 120 | $this->alternativeMethod 121 | ), 122 | $this->identifier 123 | ); 124 | } 125 | 126 | // Check for cast in expr 127 | if ($node->expr instanceof Node\Expr\Cast) { 128 | $innerType = $scope->getType($node->expr->expr); 129 | if ($this->isRestrictedType($innerType)) { 130 | return $this->createError( 131 | sprintf( 132 | "Arithmetic assignment operations with %s objects (even after casting) are prohibited. " . 133 | "Use %s methods instead.", 134 | $this->componentName, 135 | $this->alternativeMethod 136 | ), 137 | $this->identifier 138 | ); 139 | } 140 | } 141 | 142 | return []; 143 | } 144 | 145 | /** 146 | * Checks if the type is among the restricted ones 147 | */ 148 | protected function isRestrictedType(Type $type): bool 149 | { 150 | foreach ($this->restrictedTypes as $restrictedType) { 151 | if ($type instanceof $restrictedType) { 152 | return true; 153 | } 154 | } 155 | 156 | return false; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Oro/Rules/Math/ProhibitBrickMathOperationsRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ProhibitBrickMathOperationsRule extends MathTypeOperationsRule 19 | { 20 | public function __construct() 21 | { 22 | $this->restrictedTypes = [ 23 | BrickMathFloatType::class, 24 | BrickMathIntegerType::class, 25 | BrickMathStringType::class 26 | ]; 27 | 28 | $this->componentName = 'BrickMath'; 29 | $this->alternativeMethod = 'Brick\\Math'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Oro/Rules/Math/ProhibitOroMathOperationsRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ProhibitOroMathOperationsRule extends MathTypeOperationsRule 19 | { 20 | public function __construct() 21 | { 22 | $this->restrictedTypes = [ 23 | OroMathFloatType::class, 24 | OroMathIntegerType::class, 25 | OroMathStringType::class 26 | ]; 27 | 28 | $this->componentName = 'OroMath'; 29 | $this->alternativeMethod = 'Oro\\Component\\Math'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Oro/Rules/Math/UnsafeMathOperationsRule.php: -------------------------------------------------------------------------------- 1 | 21 | * 22 | * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) 23 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 24 | * @SuppressWarnings(PHPMD.NPathComplexity) 25 | */ 26 | class UnsafeMathOperationsRule extends BaseMathOperationsRule 27 | { 28 | public function processNode(Node $node, Scope $scope): array 29 | { 30 | if ($this->isMathOperation($node)) { 31 | return $this->processBinaryOp($node, $scope); 32 | } 33 | 34 | if ($this->isAssignOp($node)) { 35 | return $this->processAssignOp($node, $scope); 36 | } 37 | 38 | return []; 39 | } 40 | 41 | /** 42 | * Process binary operations (+, -, *, /) 43 | */ 44 | protected function processBinaryOp(BinaryOp $node, Scope $scope): array 45 | { 46 | $leftType = $scope->getType($node->left); 47 | $rightType = $scope->getType($node->right); 48 | 49 | if (!$this->isNumericType($leftType) || !$this->isNumericType($rightType)) { 50 | return []; 51 | } 52 | 53 | // If the operation is precision-safe 54 | if ($this->isPrecisionSafe($node, $leftType, $rightType)) { 55 | return []; 56 | } 57 | 58 | return $this->createError( 59 | 'Mathematical operations on numeric values are prohibited. Use Brick\Math instead.', 60 | 'unsafeMathOperations.prohibitedOperation' 61 | ); 62 | } 63 | 64 | /** 65 | * Process assignment operations (+=, -=, *=) 66 | */ 67 | protected function processAssignOp(AssignOp $node, Scope $scope): array 68 | { 69 | $varType = $scope->getType($node->var); 70 | $exprType = $scope->getType($node->expr); 71 | 72 | if (!$this->isNumericType($varType) || !$this->isNumericType($exprType)) { 73 | return []; 74 | } 75 | 76 | // Create a pseudo binary operation to reuse precision safety check 77 | $pseudoBinaryOp = new BinaryOp\Plus($node->var, $node->expr, $node->getAttributes()); 78 | 79 | if ($this->isPrecisionSafe($pseudoBinaryOp, $varType, $exprType)) { 80 | return []; 81 | } 82 | 83 | return $this->createError( 84 | 'Mathematical assignment operations on numeric values are prohibited. Use Brick\Math instead.', 85 | 'unsafeMathOperations.prohibitedAssignmentOperation' 86 | ); 87 | } 88 | 89 | /** 90 | * Checks if the type is numeric 91 | */ 92 | private function isNumericType(Type $type): bool 93 | { 94 | return $type->isInteger()->yes() || $type->isFloat()->yes(); 95 | } 96 | 97 | /** 98 | * Checks if the operation is safe in terms of precision 99 | * Unsafe by default 100 | */ 101 | private function isPrecisionSafe(Node $node, Type $leftType, Type $rightType): bool 102 | { 103 | // Safe if both operands are integers 104 | if ($leftType instanceof IntegerType && $rightType instanceof IntegerType) { 105 | return true; 106 | } 107 | 108 | // Checks for IntegerRangeType and UnionType 109 | if ($leftType instanceof \PHPStan\Type\IntegerRangeType 110 | || $rightType instanceof \PHPStan\Type\IntegerRangeType 111 | || $leftType instanceof \PHPStan\Type\UnionType 112 | || $rightType instanceof \PHPStan\Type\UnionType 113 | ) { 114 | return $this->isSinglePrecisionSafe($leftType, $rightType) 115 | || $this->isSinglePrecisionSafe($rightType, $leftType); 116 | } 117 | 118 | // If left type is int|false 119 | if ($leftType->isInteger()->maybe() && $leftType->isSuperTypeOf(new IntegerType())->yes()) { 120 | return true; 121 | } 122 | 123 | // If right type is int|false 124 | if ($rightType->isInteger()->maybe() && $rightType->isSuperTypeOf(new IntegerType())->yes()) { 125 | return true; 126 | } 127 | 128 | // Operations with a variable and a constant, E.G., $count - 1 129 | if ($rightType instanceof ConstantIntegerType || $leftType instanceof ConstantIntegerType) { 130 | $constOperand = $rightType instanceof ConstantIntegerType ? $rightType : $leftType; 131 | 132 | if (($constOperand->isInteger()->yes() || $constOperand->isInteger()->maybe()) 133 | && ($leftType->isInteger()->yes() || $rightType->isInteger()->yes())) { 134 | return true; 135 | } 136 | } 137 | 138 | // Safe if both operands are floating-point numbers 139 | if ($leftType instanceof FloatType && $rightType instanceof FloatType) { 140 | return true; 141 | } 142 | 143 | // Division operation check 144 | if ($node instanceof Node\Expr\BinaryOp\Div) { 145 | // Division is safe only if the result remains an integer 146 | if ($leftType instanceof IntegerType && $rightType instanceof IntegerType) { 147 | return $this->isDivisionResultInteger($node); 148 | } 149 | // Unsafe if different types or float 150 | return false; 151 | } 152 | 153 | /** 154 | * Implicit cast check 155 | * Unsafe if implicit type conversion occurs 156 | */ 157 | if ($this->isImplicitCast($leftType, $rightType)) { 158 | return false; 159 | } 160 | 161 | // If both operands are explicitly cast to int 162 | if ($node instanceof BinaryOp && 163 | $node->left instanceof Node\Expr\Cast\Int_ && 164 | $node->right instanceof Node\Expr\Cast\Int_) { 165 | return true; 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /** 172 | * Checks if one operand is safe in terms of precision 173 | * Unsafe by default 174 | */ 175 | private function isSinglePrecisionSafe(Type $primary, Type $secondary): bool 176 | { 177 | // If both operands are IntegerType 178 | if ($primary instanceof IntegerType && $secondary instanceof IntegerType) { 179 | return true; 180 | } 181 | 182 | // If primary is IntegerRangeType 183 | if ($primary instanceof \PHPStan\Type\IntegerRangeType) { 184 | return true; 185 | } 186 | 187 | // If secondary is UnionType, check its contents 188 | if ($secondary instanceof \PHPStan\Type\UnionType) { 189 | foreach ($secondary->getTypes() as $type) { 190 | if (!$type instanceof IntegerType) { 191 | return false; 192 | } 193 | } 194 | return true; 195 | } 196 | 197 | return false; 198 | } 199 | 200 | /** 201 | * Checks if the division result is an integer 202 | */ 203 | private function isDivisionResultInteger(Node\Expr\BinaryOp\Div $node): bool 204 | { 205 | if ($node->left instanceof Node\Scalar\LNumber && $node->right instanceof Node\Scalar\LNumber) { 206 | $leftValue = $node->left->value; 207 | $rightValue = $node->right->value; 208 | 209 | // Without remainder 210 | return $rightValue !== 0 && ($leftValue % $rightValue === 0); 211 | } 212 | 213 | // For dynamic values, consider division unsafe 214 | return false; 215 | } 216 | 217 | /** 218 | * Checks for implicit type casting 219 | */ 220 | private function isImplicitCast(Type $leftType, Type $rightType): bool 221 | { 222 | return ($leftType instanceof IntegerType && $rightType instanceof FloatType) || 223 | ($leftType instanceof FloatType && $rightType instanceof IntegerType); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Oro/Rules/Methods/QueryBuilderInjectionRule.php: -------------------------------------------------------------------------------- 1 | true, 31 | 'implode' => true, 32 | 'join' => true, 33 | 'reset' => true, 34 | 'current' => true, 35 | 'strtr' => true, 36 | 'replace' => true, 37 | 'strtolower' => true, 38 | 'strtoupper' => true 39 | ]; 40 | 41 | const SAFE_FUNCTIONS = [ 42 | 'base64_encode' => true, 43 | 'count' => true 44 | ]; 45 | 46 | const VAR = 'variables'; 47 | const PROPERTIES = 'properties'; 48 | 49 | const SAFE_METHODS = 'safe_methods'; 50 | const SAFE_STATIC_METHODS = 'safe_static_methods'; 51 | const CHECK_METHODS_SAFETY = 'check_methods_safety'; 52 | const CHECK_STATIC_METHODS_SAFETY = 'check_static_methods_safety'; 53 | 54 | const CLEAR_METHODS = 'clear_methods'; 55 | const CLEAR_STATIC_METHODS = 'clear_static_methods'; 56 | 57 | const CHECK_METHODS = 'check_methods'; 58 | const ALL_METHODS = '__all__'; 59 | 60 | /** 61 | * @var \PHPStan\Rules\RuleLevelHelper 62 | */ 63 | private $ruleLevelHelper; 64 | 65 | /** 66 | * @var bool 67 | */ 68 | private $checkThisOnly; 69 | 70 | /** 71 | * @var \PhpParser\PrettyPrinter\Standard 72 | */ 73 | private $printer; 74 | 75 | /** 76 | * @var string 77 | */ 78 | private $currentFile; 79 | 80 | /** 81 | * @var array 82 | */ 83 | private $localTrustedVars = []; 84 | 85 | /** 86 | * @var array 87 | */ 88 | private $checkMethodNames = []; 89 | 90 | /** 91 | * @var array 92 | */ 93 | private $trustedData; 94 | 95 | /** 96 | * @param \PhpParser\PrettyPrinter\Standard $printer 97 | * @param RuleLevelHelper $ruleLevelHelper 98 | * @param bool $checkThisOnly 99 | * @param array $trustedData 100 | */ 101 | public function __construct( 102 | \PhpParser\PrettyPrinter\Standard $printer, 103 | RuleLevelHelper $ruleLevelHelper, 104 | bool $checkThisOnly, 105 | array $trustedData = [] 106 | ) { 107 | $this->ruleLevelHelper = $ruleLevelHelper; 108 | $this->checkThisOnly = $checkThisOnly; 109 | $this->printer = $printer; 110 | $this->loadTrustedData($trustedData); 111 | } 112 | 113 | /** 114 | * Apply for all node types, as we need to process assigns and method calls. 115 | * 116 | * {@inheritdoc} 117 | */ 118 | public function getNodeType(): string 119 | { 120 | return Node\Expr::class; 121 | } 122 | 123 | /** 124 | * @param Node\Expr\MethodCall|Node $node 125 | * @param \PHPStan\Analyser\Scope $scope 126 | * @return string[] 127 | */ 128 | public function processNode(Node $node, Scope $scope): array 129 | { 130 | if ($node instanceof Node\Expr\Assign) { 131 | $this->processAssigns($node, $scope); 132 | } elseif ($node instanceof Node\Expr\MethodCall) { 133 | return $this->processMethodCalls($node, $scope); 134 | } elseif ($node instanceof Node\Expr\StaticCall) { 135 | $this->processStaticMethodCall($node, $scope); 136 | } 137 | 138 | return []; 139 | } 140 | 141 | /** 142 | * Check that all arguments of function are safe. 143 | * 144 | * @param Node\Expr $value 145 | * @param Scope $scope 146 | * @return bool 147 | */ 148 | private function isUnsafeFunctionCall(Node\Expr $value, Scope $scope): bool 149 | { 150 | if ($value instanceof Node\Expr\FuncCall) { 151 | if ($value->name instanceof Node\Name 152 | && !empty(self::CHECK_FUNCTIONS[\strtolower($value->name->toString())])) { 153 | foreach ($value->args as $arg) { 154 | if ($this->isUnsafe($arg->value, $scope)) { 155 | return true; 156 | } 157 | } 158 | } else { 159 | if ($value->name instanceof Node\Name) { 160 | return empty(self::SAFE_FUNCTIONS[\strtolower($value->name->toString())]); 161 | } 162 | 163 | return true; 164 | } 165 | } 166 | 167 | return false; 168 | } 169 | 170 | /** 171 | * Check static method for unsafe usages. 172 | * 173 | * @param Node\Expr $value 174 | * @return bool 175 | */ 176 | private function isUnsafeStaticMethodCall(Node\Expr $value, Scope $scope): bool 177 | { 178 | if ($value instanceof Node\Expr\StaticCall && $value->class instanceof Node\Name) { 179 | $className = $value->class->toString(); 180 | if ($className === 'self') { 181 | $className = $scope->getClassReflection()?->getName(); 182 | } 183 | 184 | if ($value->name instanceof \PhpParser\Node\Expr\Variable) { 185 | return false; 186 | } 187 | $methodName = \strtolower((string)$value->name); 188 | 189 | // Whitelisted methods are safe 190 | if ($className && !empty($this->trustedData[self::SAFE_STATIC_METHODS][$className][$methodName])) { 191 | return false; 192 | } 193 | 194 | // Check method arguments for safeness, if there are unsafe items - mark method as unsafe 195 | if ($className && ( 196 | $result = $this 197 | ->checkMethodArguments( 198 | $value, 199 | $scope, 200 | $className, 201 | $this->trustedData[self::CHECK_STATIC_METHODS_SAFETY] 202 | ) 203 | ) !== null 204 | ) { 205 | return $result; 206 | } 207 | 208 | return true; 209 | } 210 | 211 | return false; 212 | } 213 | 214 | /** 215 | * Check that method is whitelisted or it's arguments are safe. 216 | * 217 | * @param Node\Expr $value 218 | * @param Scope $scope 219 | * @param array $errors 220 | * @return bool 221 | */ 222 | private function isUnsafeMethodCall(Node\Expr $value, Scope $scope, array &$errors = []): bool 223 | { 224 | $errors = []; 225 | if ($value instanceof Node\Expr\MethodCall) { 226 | if (!\is_string($value->name) && !$value->name instanceof Node\Identifier) { 227 | return true; 228 | } 229 | 230 | // Mark method safe if it's returned type is boolean or numeric 231 | $valueType = $scope->getType($value); 232 | if ($this->isSafeScalarValue($valueType)) { 233 | return false; 234 | } 235 | 236 | // Check only methods that are called on an object or $this 237 | $type = $scope->getType($value->var); 238 | if (!$type instanceof ObjectType && !$type instanceof ThisType && !$type instanceof UnionType) { 239 | if (in_array(strtolower((string)$value->name), $this->checkMethodNames, true)) { 240 | $errors[] = \sprintf( 241 | 'Could not determine type for %s. ' . PHP_EOL . 242 | 'Class %s, method %s', 243 | $this->printer->prettyPrintExpr($value), 244 | $scope->getClassReflection()?->getName(), 245 | $scope->getFunctionName() 246 | ); 247 | } 248 | 249 | return true; 250 | } 251 | 252 | if ($type instanceof UnionType) { 253 | $typeFound = false; 254 | foreach ($type->getTypes() as $possibleType) { 255 | $typeFound = $possibleType instanceof ObjectType || $possibleType instanceof ThisType; 256 | if ($typeFound) { 257 | $type = $possibleType; 258 | break; 259 | } 260 | } 261 | 262 | if (!$typeFound) { 263 | return true; 264 | } 265 | } 266 | 267 | $className = $type->getClassName(); 268 | $this->checkClearMethodCall(self::CLEAR_METHODS, $className, $value, $scope); 269 | 270 | if (!\is_string($value->name) && !$value->name instanceof Node\Identifier) { 271 | return true; 272 | } 273 | 274 | // Consider Doctrine\ORM\EntityRepository::getEntityName as safe 275 | if ((string)$value->name === 'getEntityName' && \is_a($className, 'Doctrine\ORM\EntityRepository', true)) { 276 | return false; 277 | } 278 | 279 | // Whitelisted methods are safe 280 | if (!empty($this->trustedData[self::SAFE_METHODS][$className][\strtolower((string)$value->name)])) { 281 | return false; 282 | } 283 | 284 | // Methods marked for checked with safe arguments are safe 285 | if (( 286 | $result = $this 287 | ->checkMethodArguments( 288 | $value, 289 | $scope, 290 | $className, 291 | $this->trustedData[self::CHECK_METHODS_SAFETY] 292 | ) 293 | ) !== null 294 | || 295 | ( 296 | $result = $this 297 | ->checkMethodArguments( 298 | $value, 299 | $scope, 300 | $className, 301 | $this->trustedData[self::CHECK_METHODS], 302 | $errors 303 | ) 304 | ) !== null 305 | ) { 306 | return $result; 307 | } 308 | 309 | // All unchecked methods are unsafe 310 | return true; 311 | } 312 | 313 | return false; 314 | } 315 | 316 | /** 317 | * @param Node\Expr|Node\Expr\MethodCall|Node\Expr\StaticCall $value 318 | * @param Scope $scope 319 | * @param string $className 320 | * @param array $config 321 | * @param array $errors 322 | * @return bool|null 323 | */ 324 | private function checkMethodArguments( 325 | Node\Expr $value, 326 | Scope $scope, 327 | string $className, 328 | array $config, 329 | array &$errors = [], 330 | bool $isConstructor = false 331 | ) { 332 | if (isset($config[$className])) { 333 | $methodName = $isConstructor ? '__construct' : (string)$value->name; 334 | $checkArg = function ($pos, array &$errors = []) use ($className, $value, $scope, $methodName) { 335 | $checkKeys = true; 336 | $checkValues = true; 337 | if (is_string($pos) && str_contains($pos, ':')) { 338 | [$pos, $type] = explode(':', $pos); 339 | if ($type === 'keys') { 340 | $checkValues = false; 341 | } 342 | if ($type === 'values') { 343 | $checkKeys = false; 344 | } 345 | } 346 | if ($this->isUnsafe($value->args[$pos]->value, $scope, $checkKeys, $checkValues)) { 347 | $errors[] = \sprintf( 348 | 'Unsafe calling method %s::%s. ' . PHP_EOL . 349 | 'Argument %d contains unsafe values %s. ' . PHP_EOL . 350 | 'Class %s, method %s', 351 | $className, 352 | $methodName, 353 | $pos, 354 | $this->printer->prettyPrint([$value->args[$pos]]), 355 | $scope->getClassReflection()?->getName(), 356 | $scope->getFunctionName() 357 | ); 358 | } 359 | }; 360 | 361 | $argsCount = \count($value->args); 362 | $lowerMethodName = \strtolower($methodName); 363 | 364 | // If method is listed in check methods and only certain arguments should be checked - check them 365 | if (isset($config[$className][$lowerMethodName]) && \is_array($config[$className][$lowerMethodName])) { 366 | foreach ($config[$className][$lowerMethodName] as $argNum) { 367 | if (isset($value->args[$argNum])) { 368 | $checkArg($argNum, $errors); 369 | } 370 | } 371 | } elseif ((isset($config[$className][$lowerMethodName]) && $config[$className][$lowerMethodName] === true) 372 | || !empty($config[$className][self::ALL_METHODS]) 373 | ) { 374 | // Check all arguments if method is marked for checks or method is in class marked for checking 375 | for ($i = 0; $i < $argsCount; $i++) { 376 | $checkArg($i, $errors); 377 | } 378 | } 379 | 380 | // If there are errors consider method as unsafe 381 | if (!empty($errors)) { 382 | // Trusted variable that was modified by unsafe method should become untrusted 383 | $unsafeVar = $this->getRootVariable($value); 384 | if ($unsafeVar) { 385 | $this->untrustVariable($unsafeVar, $scope); 386 | } 387 | 388 | return true; 389 | } 390 | 391 | return false; 392 | } 393 | 394 | return null; 395 | } 396 | 397 | /** 398 | * Check that all parts of concat are safe. 399 | * 400 | * @param Node\Expr $value 401 | * @param Scope $scope 402 | * @return bool 403 | */ 404 | private function isUnsafeConcat(Node\Expr $value, Scope $scope): bool 405 | { 406 | if ($value instanceof Node\Expr\BinaryOp\Concat) { 407 | return $this->isUnsafe($value->left, $scope) || $this->isUnsafe($value->right, $scope); 408 | } 409 | 410 | return false; 411 | } 412 | 413 | 414 | private function isUnsafeNew(Node\Expr $value, Scope $scope) 415 | { 416 | if (!$value instanceof Node\Expr\New_ || !$value->class instanceof Node\Name) { 417 | return false; 418 | } 419 | 420 | $errors = []; 421 | 422 | return $this->checkMethodArguments( 423 | $value, 424 | $scope, 425 | $value->class->toString(), 426 | $this->trustedData[self::CHECK_METHODS_SAFETY], 427 | $errors, 428 | true 429 | ); 430 | } 431 | 432 | /** 433 | * Check that variable is whitelisted or was considered safe during assignment. 434 | * 435 | * @param Node\Expr $value 436 | * @param Scope $scope 437 | * @return bool 438 | */ 439 | private function isUnsafeVariable(Node\Expr $value, Scope $scope): bool 440 | { 441 | if ($value instanceof Node\Expr\Variable) { 442 | $className = $scope->getClassReflection()?->getName(); 443 | if (!$className) { 444 | return false; 445 | } 446 | if ($this->isSafeScalarValue($scope->getType($value))) { 447 | return false; 448 | } 449 | 450 | $functionName = \strtolower((string)$scope->getFunctionName()); 451 | $varName = \strtolower((string)$value->name); 452 | 453 | return empty($this->trustedData[self::VAR][$className][$functionName][$varName]) 454 | && empty($this->localTrustedVars[$scope->getFile()][$functionName][$varName]); 455 | } 456 | 457 | return false; 458 | } 459 | 460 | /** 461 | * Check that property is whitelisted or it is _entityName of some repository. 462 | * 463 | * @param Node\Expr $value 464 | * @param Scope $scope 465 | * @return bool 466 | */ 467 | private function isUnsafeProperty(Node\Expr $value, Scope $scope): bool 468 | { 469 | if ($value instanceof Node\Expr\PropertyFetch 470 | && (is_string($value->name) || $value->name instanceof Node\Identifier) 471 | ) { 472 | $type = $scope->getType($value->var); 473 | if (!$type instanceof ObjectType && !$type instanceof ThisType) { 474 | return true; 475 | } 476 | if ($this->isSafeScalarValue($scope->getType($value))) { 477 | return false; 478 | } 479 | 480 | $className = $type->getClassName(); 481 | 482 | if ((string)$value->name === '_entityName' && \is_a($className, 'Doctrine\ORM\EntityRepository', true)) { 483 | return false; 484 | } 485 | 486 | $functionName = \strtolower((string)$scope->getFunctionName()); 487 | $varName = \strtolower((string)$value->name); 488 | 489 | return empty($this->trustedData[self::PROPERTIES][$className][$functionName][$varName]); 490 | } 491 | 492 | return false; 493 | } 494 | 495 | /** 496 | * Check that all parts of encapsed are safe. 497 | * 498 | * @param Node\Expr $value 499 | * @param Scope $scope 500 | * @return bool 501 | */ 502 | private function isUnsafeEncapsedString(Node\Expr $value, Scope $scope): bool 503 | { 504 | if ($value instanceof Node\Scalar\Encapsed) { 505 | foreach ($value->parts as $partValue) { 506 | if ($this->isUnsafe($partValue, $scope)) { 507 | return true; 508 | } 509 | } 510 | } 511 | 512 | return false; 513 | } 514 | 515 | /** 516 | * Only checked types may be safe. All unchecked types are considered as unsafe by default. 517 | * 518 | * @param Node\Expr $value 519 | * @return bool 520 | */ 521 | private function isUncheckedType(Node\Expr $value): bool 522 | { 523 | return !( 524 | $value instanceof Node\Expr\MethodCall 525 | || $value instanceof Node\Expr\FuncCall 526 | || $value instanceof Node\Expr\BinaryOp\Concat 527 | || $value instanceof Node\Expr\Variable 528 | || $value instanceof Node\Expr\PropertyFetch 529 | || $value instanceof Node\Scalar\Encapsed 530 | || $value instanceof Node\Scalar\EncapsedStringPart 531 | || $value instanceof Node\Scalar\String_ 532 | || $value instanceof Node\Scalar\DNumber 533 | || $value instanceof Node\Scalar\LNumber 534 | || $value instanceof Node\Expr\StaticCall 535 | || $value instanceof Node\Expr\ClassConstFetch 536 | || $value instanceof Node\Expr\ArrayDimFetch 537 | || $value instanceof Node\Expr\ConstFetch 538 | || $value instanceof Node\Expr\Array_ 539 | || $value instanceof Node\Expr\Cast 540 | || $value instanceof Node\Expr\Ternary 541 | || $value instanceof Node\Expr\New_ 542 | ); 543 | } 544 | 545 | /** 546 | * Check that array dim is safe. 547 | * 548 | * @param Node\Expr $value 549 | * @param Scope $scope 550 | * @return bool 551 | */ 552 | private function isUnsafeArrayDimFetch(Node\Expr $value, Scope $scope): bool 553 | { 554 | if ($value instanceof Node\Expr\ArrayDimFetch) { 555 | return $this->isUnsafe($value->var, $scope); 556 | } 557 | 558 | return false; 559 | } 560 | 561 | /** 562 | * Check that all array elements are safe. 563 | * 564 | * @param Node\Expr $value 565 | * @param Scope $scope 566 | * @return bool 567 | */ 568 | private function isUnsafeArray( 569 | Node\Expr $value, 570 | Scope $scope, 571 | bool $checkKeys = true, 572 | bool $checkValues = true 573 | ): bool { 574 | if ($value instanceof Node\Expr\Array_) { 575 | foreach ($value->items as $arrayItem) { 576 | if ($checkKeys && $arrayItem->key && $this->isUnsafe($arrayItem->key, $scope)) { 577 | return true; 578 | } 579 | 580 | if ($checkValues && $this->isUnsafe($arrayItem->value, $scope)) { 581 | return true; 582 | } 583 | } 584 | } 585 | 586 | return false; 587 | } 588 | 589 | /** 590 | * Consider variables casted to boolean or numeric as safe. 591 | * 592 | * @param Node\Expr $value 593 | * @param Scope $scope 594 | * @return bool 595 | */ 596 | private function isUnsafeCast(Node\Expr $value, Scope $scope): bool 597 | { 598 | if ($value instanceof Node\Expr\Cast) { 599 | if ($value instanceof Node\Expr\Cast\Int_ 600 | || $value instanceof Node\Expr\Cast\Bool_ 601 | || $value instanceof Node\Expr\Cast\Double 602 | ) { 603 | return false; 604 | } 605 | 606 | return $this->isUnsafe($value->expr, $scope); 607 | } 608 | 609 | return false; 610 | } 611 | 612 | /** 613 | * Check that if-else branches of ternary operator are safe. 614 | * 615 | * @param Node\Expr $value 616 | * @param Scope $scope 617 | * @return bool 618 | */ 619 | private function isUnsafeTernary(Node\Expr $value, Scope $scope): bool 620 | { 621 | if ($value instanceof Node\Expr\Ternary) { 622 | return ($value->if && $this->isUnsafe($value->if, $scope)) || $this->isUnsafe($value->else, $scope); 623 | } 624 | 625 | return false; 626 | } 627 | 628 | /** 629 | * Check node safety. 630 | * 631 | * @param Node\Expr $value 632 | * @param Scope $scope 633 | * @return bool 634 | */ 635 | private function isUnsafe( 636 | Node\Expr $value, 637 | Scope $scope, 638 | bool $checkKeys = true, 639 | bool $checkValues = true 640 | ): bool { 641 | return $this->isUncheckedType($value) 642 | || $this->isUnsafeNew($value, $scope) 643 | || $this->isUnsafeVariable($value, $scope) 644 | || $this->isUnsafeProperty($value, $scope) 645 | || $this->isUnsafeStaticMethodCall($value, $scope) 646 | || $this->isUnsafeFunctionCall($value, $scope) 647 | || $this->isUnsafeMethodCall($value, $scope) 648 | || $this->isUnsafeArrayDimFetch($value, $scope) 649 | || $this->isUnsafeConcat($value, $scope) 650 | || $this->isUnsafeEncapsedString($value, $scope) 651 | || $this->isUnsafeCast($value, $scope) 652 | || $this->isUnsafeArray($value, $scope, $checkKeys, $checkValues) 653 | || $this->isUnsafeTernary($value, $scope); 654 | } 655 | 656 | /** 657 | * Gather information about variable safety during assignment. 658 | * 659 | * @param Node\Expr\Assign $node 660 | * @param Scope $scope 661 | */ 662 | private function processAssigns(Node\Expr\Assign $node, Scope $scope) 663 | { 664 | if ($this->currentFile !== $scope->getFile()) { 665 | unset($this->localTrustedVars[$this->currentFile]); 666 | $this->currentFile = $scope->getFile(); 667 | } 668 | 669 | $isUnsafe = $this->isUnsafe($node->expr, $scope); 670 | /** @var Node\Expr\Variable $var */ 671 | if (($var = $node->var) instanceof Node\Expr\Variable) { 672 | if ($isUnsafe) { 673 | // Do not trust unsafe variables 674 | $this->untrustVariable($var, $scope); 675 | } else { 676 | // Trust safe variables 677 | $this->trustVariable($var, $scope); 678 | } 679 | } elseif ($node->var instanceof Node\Expr\List_) { 680 | foreach ($node->var->items as $item) { 681 | if ($item instanceof Node\Expr\ArrayItem && $item->value instanceof Node\Expr\Variable) { 682 | if ($isUnsafe) { 683 | $this->untrustVariable($item->value, $scope); 684 | } else { 685 | $this->trustVariable($item->value, $scope); 686 | } 687 | } 688 | } 689 | } 690 | } 691 | 692 | /** 693 | * @param Node\Expr\MethodCall $node 694 | * @param Scope $scope 695 | * @return array 696 | */ 697 | protected function processMethodCalls(Node\Expr\MethodCall $node, Scope $scope): array 698 | { 699 | if (!\is_string($node->name) && !$node->name instanceof Node\Identifier) { 700 | return []; 701 | } 702 | 703 | if ($this->checkThisOnly && !$this->ruleLevelHelper->isThis($node->var)) { 704 | return []; 705 | } 706 | 707 | $errors = []; 708 | $this->isUnsafeMethodCall($node, $scope, $errors); 709 | 710 | return $errors; 711 | } 712 | 713 | /** 714 | * @param Node $node 715 | * @param Scope $scope 716 | */ 717 | private function processStaticMethodCall(Node $node, Scope $scope) 718 | { 719 | if ($node instanceof Node\Expr\StaticCall && $node->class instanceof Node\Name) { 720 | $className = $node->class->toString(); 721 | if ($className === 'self') { 722 | $className = $scope->getClassReflection()?->getName(); 723 | } 724 | if ($className) { 725 | $this->checkClearMethodCall(self::CLEAR_STATIC_METHODS, $className, $node, $scope); 726 | } 727 | } 728 | } 729 | 730 | /** 731 | * Trust variables checked by clear methods 732 | * 733 | * @param string $type 734 | * @param Node|Node\Expr\StaticCall|Node\Expr\MethodCall $value 735 | * @param Scope $scope 736 | */ 737 | private function checkClearMethodCall($type, $className, Node $value, Scope $scope) 738 | { 739 | if (!$value->name instanceof \PhpParser\Node\Expr\Variable 740 | && !empty($this->trustedData[$type][$className][\strtolower((string)$value->name)]) 741 | && $value->args[0]->value instanceof Node\Expr\Variable 742 | ) { 743 | $this->trustVariable($value->args[0]->value, $scope); 744 | } 745 | } 746 | 747 | /** 748 | * Load trusted data. 749 | * Convert all function and variable names to lower case. 750 | * 751 | * @param array $loadedData 752 | */ 753 | protected function loadTrustedData(array $loadedData) 754 | { 755 | // Load trusted_data.neon files 756 | $configs = [$loadedData]; 757 | foreach (\Oro\TrustedDataConfigurationFinder::findFiles() as $file) { 758 | $config = Neon::decode(file_get_contents($file)); 759 | if (!array_key_exists('trusted_data', $config)) { 760 | continue; 761 | } 762 | $configs[] = $config['trusted_data']; 763 | } 764 | $loadedData = array_merge_recursive(...$configs); 765 | 766 | $data = []; 767 | 768 | $lowerVariables = function ($loaded, $key) use (&$data) { 769 | foreach ($loaded[$key] as $class => $methods) { 770 | foreach ($methods as $method => $variables) { 771 | $lowerMethod = \strtolower($method); 772 | $data[$key][$class][$lowerMethod] = []; 773 | foreach ($variables as $variable => $varData) { 774 | $data[$key][$class][$lowerMethod][strtolower($variable)] = $varData; 775 | } 776 | } 777 | } 778 | }; 779 | $lowerVariables($loadedData, self::VAR); 780 | $lowerVariables($loadedData, self::PROPERTIES); 781 | 782 | $lowerMethods = function ($loaded, $key) use (&$data) { 783 | if (empty($loaded[$key])) { 784 | $data[$key] = []; 785 | } else { 786 | foreach ($loaded[$key] as $class => $methods) { 787 | foreach ($methods as $method => $methodData) { 788 | $data[$key][$class][strtolower($method)] = $methodData; 789 | } 790 | } 791 | } 792 | }; 793 | $lowerMethods($loadedData, self::SAFE_METHODS); 794 | $lowerMethods($loadedData, self::CHECK_METHODS_SAFETY); 795 | $lowerMethods($loadedData, self::CHECK_STATIC_METHODS_SAFETY); 796 | $lowerMethods($loadedData, self::SAFE_STATIC_METHODS); 797 | $lowerMethods($loadedData, self::CHECK_METHODS); 798 | $lowerMethods($loadedData, self::CLEAR_METHODS); 799 | $lowerMethods($loadedData, self::CLEAR_STATIC_METHODS); 800 | 801 | $this->trustedData = $data; 802 | 803 | $this->initializeCheckMethods(); 804 | } 805 | 806 | /** 807 | * @param Node\Expr\Variable $var 808 | * @param Scope $scope 809 | */ 810 | private function trustVariable(Node\Expr\Variable $var, Scope $scope) 811 | { 812 | $functionName = \strtolower((string)$scope->getFunctionName()); 813 | $varName = \strtolower((string)$var->name); 814 | $this->localTrustedVars[$scope->getFile()][$functionName][$varName] = true; 815 | } 816 | 817 | /** 818 | * @param Node\Expr\Variable $var 819 | * @param Scope $scope 820 | */ 821 | private function untrustVariable(Node\Expr\Variable $var, Scope $scope) 822 | { 823 | $functionName = \strtolower((string)$scope->getFunctionName()); 824 | $varName = \strtolower((string)$var->name); 825 | unset($this->localTrustedVars[$scope->getFile()][$functionName][$varName]); 826 | } 827 | 828 | /** 829 | * @param Node $node 830 | * @return null|Node\Expr\Variable 831 | */ 832 | private function getRootVariable(Node $node) 833 | { 834 | if ($node instanceof Node\Expr\Variable) { 835 | return $node; 836 | } elseif ($node instanceof Node\Expr\MethodCall) { 837 | return $this->getRootVariable($node->var); 838 | } 839 | 840 | return null; 841 | } 842 | 843 | /** 844 | * @param Type $valueType 845 | * @return bool 846 | */ 847 | private function isSafeScalarValue(Type $valueType): bool 848 | { 849 | if ($valueType instanceof UnionType) { 850 | foreach ($valueType->getTypes() as $subType) { 851 | if (!$this->isSafeScalarValue($subType)) { 852 | return false; 853 | } 854 | } 855 | 856 | return true; 857 | } 858 | 859 | return $valueType instanceof IntegerType 860 | || $valueType instanceof FloatType 861 | || $valueType instanceof BooleanType 862 | || $valueType instanceof NullType; 863 | } 864 | 865 | /** 866 | * Check method names are methods that MUST be checked and should trigger error when impossible to detect type. 867 | */ 868 | private function initializeCheckMethods(): void 869 | { 870 | $this->checkMethodNames = []; 871 | foreach ($this->trustedData[self::CHECK_METHODS] as $class => $methods) { 872 | $this->checkMethodNames = array_merge($this->checkMethodNames, array_keys($methods)); 873 | } 874 | 875 | // Remove `add` method as this name is too widely used and type detection fails too often 876 | $this->checkMethodNames = array_diff(array_unique($this->checkMethodNames), ['add']); 877 | } 878 | } 879 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/BrickMathReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName(); 39 | 40 | if (!in_array($declaringClass, self::SUPPORTED_CLASSES, true)) { 41 | return false; 42 | } 43 | 44 | return in_array($methodReflection->getName(), ['toFloat', 'toInt', '__toString'], true); 45 | } 46 | 47 | public function getTypeFromMethodCall( 48 | MethodReflection $methodReflection, 49 | Node\Expr\MethodCall $methodCall, 50 | Scope $scope 51 | ): ?\PHPStan\Type\Type { 52 | switch ($methodReflection->getName()) { 53 | case 'toFloat': 54 | return new BrickMathFloatType(); 55 | case 'toInt': 56 | return new BrickMathIntegerType(); 57 | case '__toString': 58 | return new BrickMathStringType(); 59 | default: 60 | throw new \LogicException('Unsupported method: ' . $methodReflection->getName()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/DoctrineConnectionReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName()); 33 | 34 | return $methodName === 'executequery'; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function getTypeFromMethodCall( 41 | MethodReflection $methodReflection, 42 | MethodCall $methodCall, 43 | Scope $scope 44 | ): Type { 45 | return new UnionType([ 46 | new ObjectType('Doctrine\DBAL\ForwardCompatibility\DriverStatement'), 47 | new ObjectType('Doctrine\DBAL\ForwardCompatibility\DriverResultStatement'), 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/EntityManagerReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | supportedClass = $supportedClass; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function getClass(): string 35 | { 36 | return $this->supportedClass; 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | public function isMethodSupported(MethodReflection $methodReflection): bool 43 | { 44 | return strtolower($methodReflection->getName()) === 'getconnection'; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function getTypeFromMethodCall( 51 | MethodReflection $methodReflection, 52 | MethodCall $methodCall, 53 | Scope $scope 54 | ): Type { 55 | return new ObjectType('Doctrine\DBAL\Connection'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/GetRepositoryDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getClassName() === 'Doctrine\Persistence\ObjectRepository') 30 | ) { 31 | return new ObjectType('Doctrine\ORM\EntityRepository'); 32 | } 33 | 34 | return $type; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/ManagerRegistryEMReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | supportedClass = $supportedClass; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function getClass(): string 35 | { 36 | return $this->supportedClass; 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | public function isMethodSupported(MethodReflection $methodReflection): bool 43 | { 44 | return strtolower($methodReflection->getName()) === 'getmanagerforclass'; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function getTypeFromMethodCall( 51 | MethodReflection $methodReflection, 52 | MethodCall $methodCall, 53 | Scope $scope 54 | ): Type { 55 | return new ObjectType('Doctrine\ORM\EntityManagerInterface'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/MathTypes/BrickMathFloatType.php: -------------------------------------------------------------------------------- 1 | supportedClass = $supportedClass; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function getClass(): string 35 | { 36 | return $this->supportedClass; 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | public function isMethodSupported(MethodReflection $methodReflection): bool 43 | { 44 | return \in_array( 45 | strtolower($methodReflection->getName()), 46 | ['createquerybuilder', 'getentitymanager'], 47 | true 48 | ); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function getTypeFromMethodCall( 55 | MethodReflection $methodReflection, 56 | MethodCall $methodCall, 57 | Scope $scope 58 | ): Type { 59 | switch (strtolower($methodReflection->getName())) { 60 | case 'createquerybuilder': 61 | return new ObjectType('Doctrine\ORM\QueryBuilder'); 62 | case 'getentitymanager': 63 | return new ObjectType('Doctrine\ORM\EntityManager'); 64 | default: 65 | throw new \InvalidArgumentException('Unsupported method call'); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/OroMathComponentReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName(); 39 | 40 | if (!in_array($declaringClass, self::SUPPORTED_CLASSES, true)) { 41 | return false; 42 | } 43 | 44 | return in_array($methodReflection->getName(), ['toFloat', 'toInteger', '__toString'], true); 45 | } 46 | 47 | public function getTypeFromMethodCall( 48 | MethodReflection $methodReflection, 49 | Node\Expr\MethodCall $methodCall, 50 | Scope $scope 51 | ): ?\PHPStan\Type\Type { 52 | switch ($methodReflection->getName()) { 53 | case 'toFloat': 54 | return new OroMathFloatType(); 55 | case 'toInteger': 56 | return new OroMathIntegerType(); 57 | case '__toString': 58 | return new OroMathStringType(); 59 | default: 60 | throw new \LogicException('Unsupported method: ' . $methodReflection->getName()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Oro/Rules/Types/RequestGetSessionTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getSession'; 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function getTypeFromMethodCall( 38 | MethodReflection $methodReflection, 39 | MethodCall $methodCall, 40 | Scope $scope 41 | ): Type { 42 | return new ObjectType('Symfony\Component\HttpFoundation\Session\Session'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Oro/Rules/ValidExceptionCatchRule.php: -------------------------------------------------------------------------------- 1 | ruleLevelHelper = $ruleLevelHelper; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getNodeType(): string 35 | { 36 | return Catch_::class; 37 | } 38 | 39 | /** 40 | * @param \PhpParser\Node\Stmt\Catch_ $node 41 | * {@inheritdoc} 42 | */ 43 | public function processNode(Node $node, Scope $scope): array 44 | { 45 | if (!$this->isValidCatchBlock($node->stmts, $scope)) { 46 | return [ 47 | 'Invalid catch block found. You should log exception or throw it.' . PHP_EOL . 48 | 'If you are certainly sure this is meant to be empty, please add ' . PHP_EOL . 49 | 'a "// @ignoreException" comment in the catch block.' 50 | ]; 51 | } 52 | 53 | return []; 54 | } 55 | 56 | /** 57 | * @param Node[] $stmts 58 | * @param Scope $scope 59 | * @return bool 60 | */ 61 | private function isValidCatchBlock(array $stmts, Scope $scope): bool 62 | { 63 | foreach ($stmts as $stmt) { 64 | //Throwing exception in catch block considered valid situation 65 | if ($stmt instanceof Node\Stmt\Throw_) { 66 | return true; 67 | } 68 | 69 | //If logger was called catch considered as valid 70 | if ($stmt instanceof Node\Expr\MethodCall) { 71 | $type = $this->ruleLevelHelper->findTypeToCheck( 72 | $scope, 73 | $stmt->var, 74 | 'Unknown class' 75 | ); 76 | 77 | if (array_key_exists(0, $type->getReferencedClasses()) && 78 | $type->getReferencedClasses()[0] === 'Psr\Log\LoggerInterface' 79 | ) { 80 | return true; 81 | } 82 | } 83 | 84 | //Comment with @ignoreException tag marks catch statement as valid 85 | if ($this->hasIgnoreComment($stmt)) { 86 | return true; 87 | } 88 | } 89 | 90 | //Otherwise catch is invalid 91 | return false; 92 | } 93 | 94 | /** 95 | * @param Node $statement 96 | * @return bool 97 | */ 98 | private function hasIgnoreComment(Node $statement): bool 99 | { 100 | $comments = []; 101 | 102 | //Try to find comments in statement 103 | if (\method_exists($statement, 'getComments')) { 104 | $comments = $statement->getComments(); 105 | } elseif ($statement->hasAttribute('comments')) { 106 | $comments = $statement->getAttribute('comments'); 107 | } 108 | 109 | foreach ($comments as $comment) { 110 | if (\strpos($comment->getText(), '@ignoreException') !== false) { 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Oro/TrustedDataConfigurationFinder.php: -------------------------------------------------------------------------------- 1 | getPrefixesPsr4(); 45 | array_walk( 46 | $prefixesPsr4, 47 | function ($dirs) use (&$directories) { 48 | $directories[] = array_values($dirs); 49 | } 50 | ); 51 | $prefixesPsr0 = $loader->getPrefixes(); 52 | array_walk( 53 | $prefixesPsr0, 54 | static function ($dirs) use (&$directories) { 55 | $directories[] = array_values($dirs); 56 | } 57 | ); 58 | $directories[] = $loader->getFallbackDirsPsr4(); 59 | $directories[] = $loader->getFallbackDirs(); 60 | } 61 | 62 | if ($directories) { 63 | $directories = array_merge(...$directories); 64 | } 65 | 66 | // Resolve directories real paths 67 | $directories = array_map('realpath', $directories); 68 | // Leave only unique records 69 | $directories = array_unique($directories); 70 | // Remove empty records 71 | $directories = array_filter($directories); 72 | 73 | return $directories; 74 | } 75 | 76 | /** 77 | * @return ClassLoader[] 78 | */ 79 | private static function getRegisteredComposerAutoloaders(): array 80 | { 81 | $composerLoaderClasses = array_filter(get_declared_classes(), static function ($className) { 82 | return strpos($className, self::COMPOSER_AUTOLOADER_INIT) === 0; 83 | }); 84 | 85 | return array_map(static function ($className) { 86 | return \call_user_func([$className, 'getLoader']); 87 | }, $composerLoaderClasses); 88 | } 89 | } 90 | --------------------------------------------------------------------------------