Skip to content

Commit

Permalink
Detect dynamic function calls (#276)
Browse files Browse the repository at this point in the history
For example:
```php
$sneaky = 'print_r';
$sneaky('foo');
```
and
```php
('print_r')('foo');
```

Ref #275
  • Loading branch information
spaze authored Dec 6, 2024
2 parents 0f030fd + e7bfee5 commit 20fd804
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 9 deletions.
39 changes: 30 additions & 9 deletions src/Calls/FunctionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\ErrorIdentifiers;

Expand All @@ -32,21 +34,30 @@ class FunctionCalls implements Rule

private ReflectionProvider $reflectionProvider;

private Normalizer $normalizer;


/**
* @param DisallowedCallsRuleErrors $disallowedCallsRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param ReflectionProvider $reflectionProvider
* @param Normalizer $normalizer
* @param array $forbiddenCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
* @throws ShouldNotHappenException
*/
public function __construct(DisallowedCallsRuleErrors $disallowedCallsRuleErrors, DisallowedCallFactory $disallowedCallFactory, ReflectionProvider $reflectionProvider, array $forbiddenCalls)
{
public function __construct(
DisallowedCallsRuleErrors $disallowedCallsRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
ReflectionProvider $reflectionProvider,
Normalizer $normalizer,
array $forbiddenCalls
) {
$this->disallowedCallsRuleErrors = $disallowedCallsRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
$this->reflectionProvider = $reflectionProvider;
$this->normalizer = $normalizer;
}


Expand All @@ -64,25 +75,35 @@ public function getNodeType(): string
*/
public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Name)) {
if ($node->name instanceof Name) {
$namespacedName = $node->name->getAttribute('namespacedName');
if ($namespacedName !== null && !($namespacedName instanceof Name)) {
throw new ShouldNotHappenException();
}
$names = [$namespacedName, $node->name];
} elseif ($node->name instanceof String_) {
$names = [new Name($this->normalizer->normalizeNamespace($node->name->value))];
} elseif ($node->name instanceof Node\Expr\Variable && is_string($node->name->name)) {
$value = $scope->getVariableType($node->name->name)->getConstantScalarValues()[0];
if (!is_string($value)) {
return [];
}
$names = [new Name($this->normalizer->normalizeNamespace($value))];
} else {
return [];
}
$namespacedName = $node->name->getAttribute('namespacedName');
if ($namespacedName !== null && !($namespacedName instanceof Name)) {
throw new ShouldNotHappenException();
}
$displayName = $node->name->getAttribute('originalName');
if ($displayName !== null && !($displayName instanceof Name)) {
throw new ShouldNotHappenException();
}
foreach ([$namespacedName, $node->name] as $name) {
foreach ($names as $name) {
if ($name && $this->reflectionProvider->hasFunction($name, $scope)) {
$functionReflection = $this->reflectionProvider->getFunction($name, $scope);
$definedIn = $functionReflection->isBuiltin() ? null : $functionReflection->getFileName();
} else {
$definedIn = null;
}
$message = $this->disallowedCallsRuleErrors->get($node, $scope, (string)$name, (string)($displayName ?? $node->name), $definedIn, $this->disallowedCalls, ErrorIdentifiers::DISALLOWED_FUNCTION);
$message = $this->disallowedCallsRuleErrors->get($node, $scope, (string)$name, (string)($displayName ?? $name), $definedIn, $this->disallowedCalls, ErrorIdentifiers::DISALLOWED_FUNCTION);
if ($message) {
return $message;
}
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsAllowInFunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsAllowInFunctionsTest extends RuleTestCase
Expand All @@ -22,6 +23,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => 'md*()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsAllowInMethodsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsAllowInMethodsTest extends RuleTestCase
Expand All @@ -22,6 +23,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => 'md5_file()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsDefinedInTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsDefinedInTest extends RuleTestCase
Expand All @@ -22,6 +23,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => '\\Foo\\Bar\\Waldo\\f*()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsInMultipleNamespacesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsInMultipleNamespacesTest extends RuleTestCase
Expand All @@ -22,6 +23,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => '__()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsNamedParamsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

/**
Expand All @@ -27,6 +28,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => 'Foo\Bar\Waldo\foo()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsParamsMessagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsParamsMessagesTest extends RuleTestCase
Expand All @@ -22,6 +23,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => '\Foo\Bar\Waldo\config()',
Expand Down
34 changes: 34 additions & 0 deletions tests/Calls/FunctionCallsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;
use Waldo\Quux\Blade;

Expand All @@ -23,6 +24,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => '\var_dump()',
Expand Down Expand Up @@ -304,6 +306,38 @@ public function testRule(): void
'Calling Foo\Bar\Waldo\config() is forbidden.',
91,
],
[
'Calling print_r() is forbidden, nope.',
102,
],
[
'Calling print_r() is forbidden, nope.',
103,
],
[
'Calling print_r() is forbidden, nope.',
106,
],
[
'Calling Print_R() is forbidden, nope. [Print_R() matches print_r()]',
107,
],
[
'Calling Foo\Bar\waldo() is forbidden, whoa, a namespace.',
110,
],
[
'Calling Foo\Bar\waldo() is forbidden, whoa, a namespace.',
111,
],
[
'Calling Foo\Bar\waldo() is forbidden, whoa, a namespace.',
114,
],
[
'Calling Foo\Bar\Waldo() is forbidden, whoa, a namespace. [Foo\Bar\Waldo() matches Foo\Bar\waldo()]',
115,
],
]);
$this->analyse([__DIR__ . '/../src/disallowed-allow/functionCalls.php'], [
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\PHPStanTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsTypeStringParamsInvalidFlagsConfigTest extends PHPStanTestCase
Expand All @@ -23,6 +24,7 @@ public function testException(): void
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => '\Foo\Bar\Waldo\intParam1()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsTypeStringParamsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\RuleTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsTypeStringParamsTest extends RuleTestCase
Expand All @@ -22,6 +23,7 @@ protected function getRule(): Rule
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => '\Foo\Bar\Waldo\config()',
Expand Down
2 changes: 2 additions & 0 deletions tests/Calls/FunctionCallsUnsupportedParamConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\PHPStanTestCase;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;

class FunctionCallsUnsupportedParamConfigTest extends PHPStanTestCase
Expand All @@ -23,6 +24,7 @@ public function testUnsupportedArrayInParamConfig(): void
$container->getByType(DisallowedCallsRuleErrors::class),
$container->getByType(DisallowedCallFactory::class),
$this->createReflectionProvider(),
$container->getByType(Normalizer::class),
[
[
'function' => [
Expand Down
17 changes: 17 additions & 0 deletions tests/src/disallowed-allow/functionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,20 @@

// allowed by path
shell_by();

// allowed by path
$sneaky = 'print_r';
$sneaky('foo');
('print_r')('foo');

$sneaky = '\print_r';
$sneaky('foo');
('\Print_R')('foo');

$sneaky = 'Foo\Bar\waldo';
$sneaky('foo');
('Foo\Bar\waldo')('foo');

$sneaky = '\Foo\Bar\waldo';
$sneaky('foo');
('\Foo\Bar\Waldo')('foo');
17 changes: 17 additions & 0 deletions tests/src/disallowed/functionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,20 @@

// would match shell_* but is excluded
shell_by();

// disallowed
$sneaky = 'print_r';
$sneaky('foo');
('print_r')('foo');

$sneaky = '\print_r';
$sneaky('foo');
('\Print_R')('foo');

$sneaky = 'Foo\Bar\waldo';
$sneaky('foo');
('Foo\Bar\waldo')('foo');

$sneaky = '\Foo\Bar\waldo';
$sneaky('foo');
('\Foo\Bar\Waldo')('foo');

0 comments on commit 20fd804

Please sign in to comment.