Skip to content

Commit

Permalink
Merge pull request #36 from spaze/spaze/detect-eval
Browse files Browse the repository at this point in the history
Detect eval()
  • Loading branch information
spaze committed Oct 18, 2020
2 parents 8474658 + cf3ed06 commit 6f829ba
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ parameters:
```
The wildcard must be the leftmost character of the function or method name, optionally followed by `()`.

You can treat `eval()` as a function (although it's a language construct) and disallow it in `disallowedFunctionCalls`.

## Example output

```
Expand Down
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ services:
factory: Spaze\PHPStan\Rules\Disallowed\StaticCalls(forbiddenCalls: %disallowedStaticCalls%)
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\EvalCalls(forbiddenCalls: %disallowedFunctionCalls%)
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\FunctionCalls(forbiddenCalls: %disallowedFunctionCalls%)
tags:
Expand Down
25 changes: 14 additions & 11 deletions src/DisallowedHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
namespace Spaze\PHPStan\Rules\Disallowed;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
Expand Down Expand Up @@ -35,34 +34,38 @@ public function __construct(FileHelper $fileHelper)

/**
* @param Scope $scope
* @param Arg[] $args
* @param FuncCall|MethodCall|StaticCall|null $node
* @param DisallowedCall $disallowedCall
* @return boolean
*/
public function isAllowed(Scope $scope, array $args, DisallowedCall $disallowedCall): bool
private function isAllowed(Scope $scope, ?Node $node, DisallowedCall $disallowedCall): bool
{
foreach ($disallowedCall->getAllowIn() as $allowedPath) {
$match = fnmatch($this->fileHelper->absolutizePath($allowedPath), $scope->getFile());
if ($match && $this->hasAllowedParams($scope, $args, $disallowedCall->getAllowParamsInAllowed(), true)) {
if ($match && $this->hasAllowedParams($scope, $node, $disallowedCall->getAllowParamsInAllowed(), true)) {
return true;
}
}
return $this->hasAllowedParams($scope, $args, $disallowedCall->getAllowParamsAnywhere(), false);
return $this->hasAllowedParams($scope, $node, $disallowedCall->getAllowParamsAnywhere(), false);
}


/**
* @param Scope $scope
* @param Arg[] $args
* @param FuncCall|MethodCall|StaticCall|null $node
* @param array<integer, integer|boolean|string> $allowConfig
* @param boolean $default
* @return boolean
*/
private function hasAllowedParams(Scope $scope, array $args, array $allowConfig, bool $default): bool
private function hasAllowedParams(Scope $scope, ?Node $node, array $allowConfig, bool $default): bool
{
if (!$node) {
return $default;
}

$disallowed = false;
foreach ($allowConfig as $param => $value) {
$arg = $args[$param - 1] ?? null;
$arg = $node->args[$param - 1] ?? null;
$type = $arg ? $scope->getType($arg->value) : null;
if ($arg && $type instanceof ConstantScalarType) {
$disallowed = $disallowed || ($value !== $type->getValue());
Expand Down Expand Up @@ -103,17 +106,17 @@ public function createCallsFromConfig(array $config): array


/**
* @param FuncCall|MethodCall|StaticCall $node
* @param FuncCall|MethodCall|StaticCall|null $node
* @param Scope $scope
* @param string $name
* @param string|null $displayName
* @param DisallowedCall[] $disallowedCalls
* @return string[]
*/
public function getDisallowedMessage(Node $node, Scope $scope, string $name, ?string $displayName, array $disallowedCalls): array
public function getDisallowedMessage(?Node $node, Scope $scope, string $name, ?string $displayName, array $disallowedCalls): array
{
foreach ($disallowedCalls as $disallowedCall) {
if ($this->callMatches($disallowedCall, $name) && !$this->isAllowed($scope, $node->args, $disallowedCall)) {
if ($this->callMatches($disallowedCall, $name) && !$this->isAllowed($scope, $node, $disallowedCall)) {
return [
sprintf(
'Calling %s is forbidden, %s%s',
Expand Down
56 changes: 56 additions & 0 deletions src/EvalCalls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);

namespace Spaze\PHPStan\Rules\Disallowed;

use PhpParser\Node;
use PhpParser\Node\Expr\Eval_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;

/**
* Reports on dynamically calling eval().
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<Eval_>
*/
class EvalCalls implements Rule
{

/** @var DisallowedHelper */
private $disallowedHelper;

/** @var DisallowedCall[] */
private $disallowedCalls;


/**
* @param DisallowedHelper $disallowedHelper
* @param array<array{function?:string, method?:string, message?:string, allowIn?:string[], allowParamsInAllowed?:array<integer, integer|boolean|string>, allowParamsAnywhere?:array<integer, integer|boolean|string>}> $forbiddenCalls
* @throws ShouldNotHappenException
*/
public function __construct(DisallowedHelper $disallowedHelper, array $forbiddenCalls)
{
$this->disallowedHelper = $disallowedHelper;
$this->disallowedCalls = $this->disallowedHelper->createCallsFromConfig($forbiddenCalls);
}


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


/**
* @param Node $node
* @param Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedHelper->getDisallowedMessage(null, $scope, 'eval', 'eval', $this->disallowedCalls);
}

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

namespace Spaze\PHPStan\Rules\Disallowed;

use PHPStan\File\FileHelper;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

class EvalCallsTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new EvalCalls(
new DisallowedHelper(new FileHelper(__DIR__)),
[
[
'function' => 'eval()',
'allowIn' => [
'src/disallowed-allowed/*.php',
'src/*-allow/*.*',
],
],
]
);
}


public function testRule(): void
{
// Based on the configuration above, in this file:
$this->analyse([__DIR__ . '/src/disallowed/functionCalls.php'], [
[
'Calling eval() is forbidden, because reasons',
28,
],
]);
// Based on the configuration above, no errors in this file:
$this->analyse([__DIR__ . '/src/disallowed-allow/functionCalls.php'], []);
}

}
3 changes: 3 additions & 0 deletions tests/src/disallowed-allow/functionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@

// allowed by path
print_r('bar bar was', false);

// a language construct, allowed by path
eval('$foo="bar";');
3 changes: 3 additions & 0 deletions tests/src/disallowed/functionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@

// disallowed, param #2 is not true
print_r('bar bar was', false);

// a disallowed language construct
eval('$foo="bar";');

0 comments on commit 6f829ba

Please sign in to comment.