1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add a proxy capability to the flow annotation (#4495)

* Add a `passthru` capability to the flow annotation

* Fix passthru-calls type

* Fix types and rename to proxy

* Allow to proxy a method

Co-authored-by: Matthew Brown <github@muglug.com>
This commit is contained in:
Adrien LUCAS 2020-11-09 21:22:35 +01:00 committed by Daniil Gentili
parent d07a8bb4a5
commit 9ab0ab9472
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
4 changed files with 143 additions and 3 deletions

View File

@ -2,6 +2,7 @@
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
use PhpParser;
use PhpParser\BuilderFactory;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
@ -1062,7 +1063,43 @@ class FunctionCallAnalyzer extends CallAnalyzer
}
if ($function_storage) {
self::taintReturnType($statements_analyzer, $stmt, $function_id, $function_storage, $stmt_type);
$return_node = self::taintReturnType(
$statements_analyzer,
$stmt,
$function_id,
$function_storage,
$stmt_type
);
if ($function_storage->proxy_calls !== null) {
foreach ($function_storage->proxy_calls as $proxy_call) {
$fake_call_arguments = [];
foreach ($proxy_call['params'] as $i) {
$fake_call_arguments[] = $stmt->args[$i];
}
$fake_call_factory = new BuilderFactory();
if (strpos($proxy_call['fqn'], '::') !== false) {
list($fqcn, $method) = explode('::', $proxy_call['fqn']);
$fake_call = $fake_call_factory->staticCall($fqcn, $method, $fake_call_arguments);
} else {
$fake_call = $fake_call_factory->funcCall($proxy_call['fqn'], $fake_call_arguments);
}
ExpressionAnalyzer::analyze($statements_analyzer, $fake_call, $context);
if (null !== $statements_analyzer->data_flow_graph
&& null !== $return_node
&& $proxy_call['return']
) {
$fake_call_type = $statements_analyzer->node_data->getType($fake_call);
if (null !== $fake_call_type) {
foreach ($fake_call_type->parent_nodes as $fake_call_node) {
$statements_analyzer->data_flow_graph->addPath($fake_call_node, $return_node, 'return');
}
}
}
}
}
}
@ -1301,11 +1338,11 @@ class FunctionCallAnalyzer extends CallAnalyzer
string $function_id,
FunctionLikeStorage $function_storage,
Type\Union $stmt_type
) : void {
) : ?DataFlowNode {
if (!$statements_analyzer->data_flow_graph instanceof TaintFlowGraph
|| \in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
) {
return;
return null;
}
$node_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
@ -1392,6 +1429,8 @@ class FunctionCallAnalyzer extends CallAnalyzer
$statements_analyzer->data_flow_graph->addSource($method_node);
}
return $function_call_node;
}
/**

View File

@ -522,6 +522,35 @@ class FunctionLikeDocblockScanner
}
}
}
if (isset($flow_parts[0]) && \strpos(trim($flow_parts[0]), 'proxy') === 0) {
$proxy_call = trim(substr($flow_parts[0], strlen('proxy')));
list($fully_qualified_name, $source_param_string) = explode('(', $proxy_call, 2);
if (!empty($fully_qualified_name) && !empty($source_param_string)) {
$source_params = preg_split('/, ?/', substr($source_param_string, 0, -1)) ?: [];
$call_params = [];
foreach ($source_params as $source_param) {
$source_param = substr($source_param, 1);
foreach ($storage->params as $i => $param_storage) {
if ($param_storage->name === $source_param) {
$call_params[] = $i;
}
}
}
if ($storage->proxy_calls === null) {
$storage->proxy_calls = [];
}
$storage->proxy_calls[] = [
'fqn' => $fully_qualified_name,
'params' => $call_params,
'return' => isset($flow_parts[1]) && trim($flow_parts[1]) === 'return'
];
}
}
}
}

View File

@ -209,6 +209,11 @@ abstract class FunctionLikeStorage
*/
public $attributes = [];
/**
* @var list<array{fqn: string, params: array<int>, return: bool}>|null
*/
public $proxy_calls = [];
public function __toString(): string
{
return $this->getSignature(false);

View File

@ -1549,6 +1549,73 @@ class TaintTest extends TestCase
echo $params["foo"];',
'error_message' => 'TaintedInput',
],
'taintFlow' => [
'<?php
/**
* @psalm-flow ($r) -> return
*/
function some_stub(string $r): string {}
$r = $_GET["untrusted"];
echo some_stub($r);',
'error_message' => 'TaintedInput',
],
'taintFlowProxy' => [
'<?php
/**
* @psalm-taint-sink text $in
*/
function dummy_taint_sink(string $in): void {}
/**
* @psalm-flow proxy dummy_taint_sink($r)
*/
function some_stub(string $r): string {}
$r = $_GET["untrusted"];
some_stub($r);',
'error_message' => 'TaintedInput',
],
'taintFlowProxyAndReturn' => [
'<?php
function dummy_taintable(string $in): string {
return $in;
}
/**
* @psalm-flow proxy dummy_taintable($r) -> return
*/
function some_stub(string $r): string {}
$r = $_GET["untrusted"];
echo some_stub($r);',
'error_message' => 'TaintedInput',
],
'taintFlowMethodProxyAndReturn' => [
'<?php
class dummy {
public function taintable(string $in): string {
return $in;
}
}
/**
* @psalm-flow proxy dummy::taintable($r) -> return
*/
function some_stub(string $r): string {}
$r = $_GET["untrusted"];
echo some_stub($r);',
'error_message' => 'TaintedInput',
]
/*
// TODO: Stubs do not support this type of inference even with $this->message = $message.
// Most uses of getMessage() would be with caught exceptions, so this is not representative of real code.