Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
| `strictArrayFilter` | Require `array_filter()` to have a callback parameter to avoid loose comparison semantics. |
| `illegalConstructorMethodCall` | Disallow calling `__construct()` on an existing object or as a static call outside of parent constructor. |
| `closureUsesThis` | Closure should use `$this` directly instead of using `$this` variable indirectly. |
| `unserializeAllowedClasses` | Calls to `unserialize()` must include the `allowed_classes` option. |

Additional rules are coming in subsequent releases!

Expand Down Expand Up @@ -74,6 +75,7 @@ parameters:
disallowedShortTernary: false
overwriteVariablesWithLoop: false
closureUsesThis: false
unserializeAllowedClasses: false
matchingInheritedMethodNames: false
numericOperandsInArithmeticOperators: false
strictFunctionCalls: false
Expand Down
7 changes: 7 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parameters:
disallowedShortTernary: %strictRules.allRules%
overwriteVariablesWithLoop: %strictRules.allRules%
closureUsesThis: %strictRules.allRules%
unserializeAllowedClasses: %strictRules.allRules%
matchingInheritedMethodNames: %strictRules.allRules%
numericOperandsInArithmeticOperators: %strictRules.allRules%
strictFunctionCalls: %strictRules.allRules%
Expand All @@ -49,6 +50,7 @@ parametersSchema:
disallowedShortTernary: anyOf(bool(), arrayOf(bool()))
overwriteVariablesWithLoop: anyOf(bool(), arrayOf(bool()))
closureUsesThis: anyOf(bool(), arrayOf(bool()))
unserializeAllowedClasses: anyOf(bool(), arrayOf(bool()))
matchingInheritedMethodNames: anyOf(bool(), arrayOf(bool()))
numericOperandsInArithmeticOperators: anyOf(bool(), arrayOf(bool()))
strictFunctionCalls: anyOf(bool(), arrayOf(bool()))
Expand Down Expand Up @@ -98,6 +100,8 @@ conditionalTags:
phpstan.rules.rule: %strictRules.strictArrayFilter%
PHPStan\Rules\Functions\ClosureUsesThisRule:
phpstan.rules.rule: %strictRules.closureUsesThis%
PHPStan\Rules\Functions\UnserializeAllowedClassesRule:
phpstan.rules.rule: %strictRules.unserializeAllowedClasses%
PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule:
phpstan.rules.rule: %strictRules.matchingInheritedMethodNames%
PHPStan\Rules\Operators\OperandInArithmeticPostDecrementRule:
Expand Down Expand Up @@ -229,6 +233,9 @@ services:
-
class: PHPStan\Rules\Functions\ClosureUsesThisRule

-
class: PHPStan\Rules\Functions\UnserializeAllowedClassesRule

-
class: PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule

Expand Down
81 changes: 81 additions & 0 deletions src/Rules/Functions/UnserializeAllowedClassesRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function count;
use function in_array;
use function strtolower;

/** @implements Rule<FuncCall> */
class UnserializeAllowedClassesRule implements Rule
{

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

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}

if (!in_array(strtolower((string) $node->name), ['unserialize'], true)) {
return [];
}

$args = $node->getArgs();

if (count($args) < 2) {
return [
RuleErrorBuilder::message(
'unserialize() called without the $options argument. Always pass '
. "['allowed_classes' => false] or an explicit list of safe classes. "
. "If you truly need to allow all classes, pass ['allowed_classes' => true] "
. 'to make that decision explicit.',
)
->identifier('unserialize.allowedClasses')
->build(),
];
}

$optionsArg = $args[1]->value;

if (!$optionsArg instanceof Node\Expr\Array_) {
return [];
}

foreach ($optionsArg->items as $item) {
if ($item === null) {

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Check failure on line 55 in src/Rules/Functions/UnserializeAllowedClassesRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, highest)

Strict comparison using === between PhpParser\Node\ArrayItem and null will always evaluate to false.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need help here. This check seems to be necessary with the oldest supported PHPStan version, but for PHPStan 2.2, it's dead code. I'm having a hard time making both PHPStans happy at the same time in your CI. 🙈

continue;
}

$key = $item->key;
if (!$key instanceof Node\Scalar\String_) {
continue;
}

if ($key->value === 'allowed_classes') {
return [];
}
}

return [
RuleErrorBuilder::message(
"unserialize() called without the 'allowed_classes' key in \$options. "
. "Always pass ['allowed_classes' => false] or an explicit list of safe classes. "
. "If you truly need to allow all classes, pass ['allowed_classes' => true] "
. 'to make that decision explicit.',
)
->identifier('unserialize.allowedClasses')
->build(),
];
}

}
39 changes: 39 additions & 0 deletions tests/Rules/Functions/UnserializeAllowedClassesRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PHPStan\Testing\RuleTestCase;

/** @extends RuleTestCase<UnserializeAllowedClassesRule> */
class UnserializeAllowedClassesRuleTest extends RuleTestCase
{

protected function getRule(): UnserializeAllowedClassesRule
{
return new UnserializeAllowedClassesRule();
}

public function testViolations(): void
{
$this->analyse([__DIR__ . '/data/unserialize-usages.php'], [
[
"unserialize() called without the \$options argument. Always pass ['allowed_classes' => false] or an explicit list of safe classes. If you truly need to allow all classes, pass ['allowed_classes' => true] to make that decision explicit.",
10,
],
[
"unserialize() called without the 'allowed_classes' key in \$options. Always pass ['allowed_classes' => false] or an explicit list of safe classes. If you truly need to allow all classes, pass ['allowed_classes' => true] to make that decision explicit.",
15,
],
[
"unserialize() called without the 'allowed_classes' key in \$options. Always pass ['allowed_classes' => false] or an explicit list of safe classes. If you truly need to allow all classes, pass ['allowed_classes' => true] to make that decision explicit.",
20,
],
]);
}

public function testNoErrors(): void
{
$this->analyse([__DIR__ . '/data/unserialize-good-usages.php'], []);
}

}
32 changes: 32 additions & 0 deletions tests/Rules/Functions/data/unserialize-good-usages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);

namespace UnserializeAllowedClasses;

final class UnserializeUsagesOk
{

/** @return array<string, mixed> */
public function allowedClassesFalse(string $data): array
{
/** @var array<string, mixed> $result */
$result = unserialize($data, ['allowed_classes' => false]);

return $result;
}

public function allowedClassesExplicitTrue(string $data): mixed
{
return unserialize($data, ['allowed_classes' => true]);
}

public function allowedClassesList(string $data): mixed
{
return unserialize($data, ['allowed_classes' => [\stdClass::class]]);
}

public function dynamicOptions(string $data, mixed $options): mixed
{
return unserialize($data, $options);
}

}
51 changes: 51 additions & 0 deletions tests/Rules/Functions/data/unserialize-usages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);

namespace UnserializeAllowedClasses;

final class UnserializeUsages
{

public function noOptions(string $data): mixed
{
return unserialize($data);
}

public function emptyOptions(string $data): mixed
{
return unserialize($data, []);
}

public function optionsWithoutAllowedClasses(string $data): mixed
{
return unserialize($data, ['some_other_key' => true]);
}

/** @return array<string, mixed> */
public function allowedClassesFalse(string $data): array
{
/** @var array<string, mixed> $result */
$result = unserialize($data, ['allowed_classes' => false]);

return $result;
}

/** @return array<string, mixed> */
public function allowedClassesExplicitTrue(string $data): array
{
/** @var array<string, mixed> $result */
$result = unserialize($data, ['allowed_classes' => true]);

return $result;
}

public function allowedClassesList(string $data): mixed
{
return unserialize($data, ['allowed_classes' => [\stdClass::class]]);
}

public function dynamicOptions(string $data, mixed $options): mixed
{
return unserialize($data, $options);
}

}
Loading