Skip to content

Commit 75bcc20

Browse files
committed
Error on unserialize() without allowed_classes option
1 parent 80870ec commit 75bcc20

6 files changed

Lines changed: 208 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
| `strictArrayFilter` | Require `array_filter()` to have a callback parameter to avoid loose comparison semantics. |
3232
| `illegalConstructorMethodCall` | Disallow calling `__construct()` on an existing object or as a static call outside of parent constructor. |
3333
| `closureUsesThis` | Closure should use `$this` directly instead of using `$this` variable indirectly. |
34+
| `unserializeAllowedClasses` | Calls to `unserialize()` must include the `allowed_classes` option. |
3435

3536
Additional rules are coming in subsequent releases!
3637

@@ -74,6 +75,7 @@ parameters:
7475
disallowedShortTernary: false
7576
overwriteVariablesWithLoop: false
7677
closureUsesThis: false
78+
unserializeAllowedClasses: false
7779
matchingInheritedMethodNames: false
7880
numericOperandsInArithmeticOperators: false
7981
strictFunctionCalls: false

rules.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ parameters:
2626
disallowedShortTernary: %strictRules.allRules%
2727
overwriteVariablesWithLoop: %strictRules.allRules%
2828
closureUsesThis: %strictRules.allRules%
29+
unserializeAllowedClasses: %strictRules.allRules%
2930
matchingInheritedMethodNames: %strictRules.allRules%
3031
numericOperandsInArithmeticOperators: %strictRules.allRules%
3132
strictFunctionCalls: %strictRules.allRules%
@@ -49,6 +50,7 @@ parametersSchema:
4950
disallowedShortTernary: anyOf(bool(), arrayOf(bool()))
5051
overwriteVariablesWithLoop: anyOf(bool(), arrayOf(bool()))
5152
closureUsesThis: anyOf(bool(), arrayOf(bool()))
53+
unserializeAllowedClasses: anyOf(bool(), arrayOf(bool()))
5254
matchingInheritedMethodNames: anyOf(bool(), arrayOf(bool()))
5355
numericOperandsInArithmeticOperators: anyOf(bool(), arrayOf(bool()))
5456
strictFunctionCalls: anyOf(bool(), arrayOf(bool()))
@@ -98,6 +100,8 @@ conditionalTags:
98100
phpstan.rules.rule: %strictRules.strictArrayFilter%
99101
PHPStan\Rules\Functions\ClosureUsesThisRule:
100102
phpstan.rules.rule: %strictRules.closureUsesThis%
103+
PHPStan\Rules\Functions\UnserializeAllowedClassesRule:
104+
phpstan.rules.rule: %strictRules.unserializeAllowedClasses%
101105
PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule:
102106
phpstan.rules.rule: %strictRules.matchingInheritedMethodNames%
103107
PHPStan\Rules\Operators\OperandInArithmeticPostDecrementRule:
@@ -229,6 +233,9 @@ services:
229233
-
230234
class: PHPStan\Rules\Functions\ClosureUsesThisRule
231235

236+
-
237+
class: PHPStan\Rules\Functions\UnserializeAllowedClassesRule
238+
232239
-
233240
class: PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule
234241

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use function count;
11+
use function in_array;
12+
use function strtolower;
13+
14+
/** @implements Rule<FuncCall> */
15+
class UnserializeAllowedClassesRule implements Rule
16+
{
17+
18+
public function getNodeType(): string
19+
{
20+
return FuncCall::class;
21+
}
22+
23+
public function processNode(Node $node, Scope $scope): array
24+
{
25+
if (!$node->name instanceof Node\Name) {
26+
return [];
27+
}
28+
29+
if (!in_array(strtolower((string) $node->name), ['unserialize'], true)) {
30+
return [];
31+
}
32+
33+
$args = $node->getArgs();
34+
35+
if (count($args) < 2) {
36+
return [
37+
RuleErrorBuilder::message(
38+
'unserialize() called without the $options argument. Always pass '
39+
. "['allowed_classes' => false] or an explicit list of safe classes. "
40+
. "If you truly need to allow all classes, pass ['allowed_classes' => true] "
41+
. 'to make that decision explicit.',
42+
)
43+
->identifier('unserialize.allowedClasses')
44+
->build(),
45+
];
46+
}
47+
48+
$optionsArg = $args[1]->value;
49+
50+
if (!$optionsArg instanceof Node\Expr\Array_) {
51+
return [];
52+
}
53+
54+
foreach ($optionsArg->items as $item) {
55+
$key = $item->key;
56+
if (!$key instanceof Node\Scalar\String_) {
57+
continue;
58+
}
59+
60+
if ($key->value === 'allowed_classes') {
61+
return [];
62+
}
63+
}
64+
65+
return [
66+
RuleErrorBuilder::message(
67+
"unserialize() called without the 'allowed_classes' key in \$options. "
68+
. "Always pass ['allowed_classes' => false] or an explicit list of safe classes. "
69+
. "If you truly need to allow all classes, pass ['allowed_classes' => true] "
70+
. 'to make that decision explicit.',
71+
)
72+
->identifier('unserialize.allowedClasses')
73+
->build(),
74+
];
75+
}
76+
77+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Testing\RuleTestCase;
6+
7+
/** @extends RuleTestCase<UnserializeAllowedClassesRule> */
8+
class UnserializeAllowedClassesRuleTest extends RuleTestCase
9+
{
10+
11+
protected function getRule(): UnserializeAllowedClassesRule
12+
{
13+
return new UnserializeAllowedClassesRule();
14+
}
15+
16+
public function testViolations(): void
17+
{
18+
$this->analyse([__DIR__ . '/data/unserialize-usages.php'], [
19+
[
20+
"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.",
21+
10,
22+
],
23+
[
24+
"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.",
25+
15,
26+
],
27+
[
28+
"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.",
29+
20,
30+
],
31+
]);
32+
}
33+
34+
public function testNoErrors(): void
35+
{
36+
$this->analyse([__DIR__ . '/data/unserialize-good-usages.php'], []);
37+
}
38+
39+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace UnserializeAllowedClasses;
4+
5+
final class UnserializeUsagesOk
6+
{
7+
8+
/** @return array<string, mixed> */
9+
public function allowedClassesFalse(string $data): array
10+
{
11+
/** @var array<string, mixed> $result */
12+
$result = unserialize($data, ['allowed_classes' => false]);
13+
14+
return $result;
15+
}
16+
17+
public function allowedClassesExplicitTrue(string $data): mixed
18+
{
19+
return unserialize($data, ['allowed_classes' => true]);
20+
}
21+
22+
public function allowedClassesList(string $data): mixed
23+
{
24+
return unserialize($data, ['allowed_classes' => [\stdClass::class]]);
25+
}
26+
27+
public function dynamicOptions(string $data, mixed $options): mixed
28+
{
29+
return unserialize($data, $options);
30+
}
31+
32+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace UnserializeAllowedClasses;
4+
5+
final class UnserializeUsages
6+
{
7+
8+
public function noOptions(string $data): mixed
9+
{
10+
return unserialize($data);
11+
}
12+
13+
public function emptyOptions(string $data): mixed
14+
{
15+
return unserialize($data, []);
16+
}
17+
18+
public function optionsWithoutAllowedClasses(string $data): mixed
19+
{
20+
return unserialize($data, ['some_other_key' => true]);
21+
}
22+
23+
/** @return array<string, mixed> */
24+
public function allowedClassesFalse(string $data): array
25+
{
26+
/** @var array<string, mixed> $result */
27+
$result = unserialize($data, ['allowed_classes' => false]);
28+
29+
return $result;
30+
}
31+
32+
/** @return array<string, mixed> */
33+
public function allowedClassesExplicitTrue(string $data): array
34+
{
35+
/** @var array<string, mixed> $result */
36+
$result = unserialize($data, ['allowed_classes' => true]);
37+
38+
return $result;
39+
}
40+
41+
public function allowedClassesList(string $data): mixed
42+
{
43+
return unserialize($data, ['allowed_classes' => [\stdClass::class]]);
44+
}
45+
46+
public function dynamicOptions(string $data, mixed $options): mixed
47+
{
48+
return unserialize($data, $options);
49+
}
50+
51+
}

0 commit comments

Comments
 (0)