diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index c9c2f59d9..349a49560 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -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; } /** diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 586634126..aab944227 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -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' + ]; + } + } } } diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 424276277..1a263c561 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -209,6 +209,11 @@ abstract class FunctionLikeStorage */ public $attributes = []; + /** + * @var list, return: bool}>|null + */ + public $proxy_calls = []; + public function __toString(): string { return $this->getSignature(false); diff --git a/tests/TaintTest.php b/tests/TaintTest.php index 4e5a5b583..206dfc9d1 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -1549,6 +1549,73 @@ class TaintTest extends TestCase echo $params["foo"];', 'error_message' => 'TaintedInput', ], + 'taintFlow' => [ + ' return + */ + function some_stub(string $r): string {} + + $r = $_GET["untrusted"]; + + echo some_stub($r);', + 'error_message' => 'TaintedInput', + ], + 'taintFlowProxy' => [ + ' 'TaintedInput', + ], + 'taintFlowProxyAndReturn' => [ + ' return + */ + function some_stub(string $r): string {} + + $r = $_GET["untrusted"]; + + echo some_stub($r);', + 'error_message' => 'TaintedInput', + ], + 'taintFlowMethodProxyAndReturn' => [ + ' 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.