1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 18:36:58 +01:00
psalm/src/Psalm/Internal/Codebase/ControlFlowGraph.php

395 lines
12 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Internal\Codebase;
use Psalm\CodeLocation;
use Psalm\Internal\ControlFlow\Path;
use Psalm\Internal\ControlFlow\TaintSink;
use Psalm\Internal\ControlFlow\TaintSource;
use Psalm\Internal\ControlFlow\ControlFlowNode;
use Psalm\IssueBuffer;
use Psalm\Issue\TaintedInput;
use function array_merge;
2020-05-22 04:47:58 +02:00
use function count;
use function implode;
use function substr;
use function strlen;
use function array_intersect;
2020-06-19 00:48:19 +02:00
use function array_reverse;
class ControlFlowGraph
{
/** @var array<string, TaintSource> */
2020-05-22 05:43:13 +02:00
private $sources = [];
/** @var array<string, ControlFlowNode> */
2020-05-22 05:43:13 +02:00
private $nodes = [];
/** @var array<string, TaintSink> */
2020-05-22 05:43:13 +02:00
private $sinks = [];
2020-06-22 08:10:03 +02:00
/** @var array<string, array<string, Path>> */
2020-05-22 05:43:13 +02:00
private $forward_edges = [];
2020-05-22 04:47:58 +02:00
/** @var array<string, array<string, true>> */
2020-05-22 05:43:13 +02:00
private $specialized_calls = [];
2020-05-25 23:10:53 +02:00
/** @var array<string, array<string, true>> */
private $specializations = [];
public function addSource(TaintSource $node) : void
{
2020-05-22 05:43:13 +02:00
$this->sources[$node->id] = $node;
}
public function addSink(TaintSink $node) : void
{
2020-05-22 05:43:13 +02:00
$this->sinks[$node->id] = $node;
2020-05-23 05:53:37 +02:00
// in the rare case the sink is the _next_ node, this is necessary
$this->nodes[$node->id] = $node;
}
public function addNode(ControlFlowNode $node) : void
{
2020-05-22 05:43:13 +02:00
$this->nodes[$node->id] = $node;
2020-05-22 04:47:58 +02:00
if ($node->unspecialized_id && $node->specialization_key) {
2020-05-22 05:43:13 +02:00
$this->specialized_calls[$node->specialization_key][$node->unspecialized_id] = true;
2020-05-25 23:10:53 +02:00
$this->specializations[$node->unspecialized_id][$node->specialization_key] = true;
2019-08-14 06:47:57 +02:00
}
}
/**
* @param array<string> $added_taints
* @param array<string> $removed_taints
2019-08-14 06:47:57 +02:00
*/
2020-05-22 04:47:58 +02:00
public function addPath(
ControlFlowNode $from,
ControlFlowNode $to,
2020-06-19 00:48:19 +02:00
string $path_type,
2020-06-26 01:12:30 +02:00
?array $added_taints = null,
?array $removed_taints = null
2019-08-14 06:47:57 +02:00
) : void {
2020-05-22 04:47:58 +02:00
$from_id = $from->id;
$to_id = $to->id;
2019-08-14 06:47:57 +02:00
2020-06-19 00:48:19 +02:00
if ($from_id === $to_id) {
return;
}
2020-06-22 08:10:03 +02:00
$this->forward_edges[$from_id][$to_id] = new Path($path_type, $added_taints, $removed_taints);
}
public function getPredecessorPath(ControlFlowNode $source) : string
{
2019-08-07 00:56:36 +02:00
$location_summary = '';
2019-08-06 23:29:44 +02:00
2019-08-07 00:56:36 +02:00
if ($source->code_location) {
$location_summary = $source->code_location->getShortSummary();
2019-08-14 15:52:59 +02:00
}
2019-08-06 23:29:44 +02:00
$source_descriptor = $source->label . ($location_summary ? ' (' . $location_summary . ')' : '');
2020-05-22 04:47:58 +02:00
$previous_source = $source->previous;
2019-08-14 06:47:57 +02:00
if ($previous_source) {
if ($previous_source === $source) {
2019-08-07 00:56:36 +02:00
return '';
}
2020-05-22 04:47:58 +02:00
return $this->getPredecessorPath($previous_source) . ' -> ' . $source_descriptor;
}
return $source_descriptor;
}
public function getSuccessorPath(ControlFlowNode $sink) : string
{
2019-08-07 00:56:36 +02:00
$location_summary = '';
2019-08-06 23:29:44 +02:00
2019-08-14 06:47:57 +02:00
if ($sink->code_location) {
$location_summary = $sink->code_location->getShortSummary();
2019-08-14 15:52:59 +02:00
}
2019-08-06 23:29:44 +02:00
$sink_descriptor = $sink->label . ($location_summary ? ' (' . $location_summary . ')' : '');
2020-05-22 04:47:58 +02:00
$next_sink = $sink->previous;
2019-08-14 06:47:57 +02:00
if ($next_sink) {
if ($next_sink === $sink) {
2019-08-07 00:56:36 +02:00
return '';
}
2020-05-22 04:47:58 +02:00
return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink);
}
2019-08-14 06:47:57 +02:00
return $sink_descriptor;
}
/**
* @return list<array{location: ?CodeLocation, label: string, entry_path_type: string}>
*/
public function getIssueTrace(ControlFlowNode $source) : array
{
$previous_source = $source->previous;
$node = [
'location' => $source->code_location,
'label' => $source->label,
'entry_path_type' => \end($source->path_types) ?: ''
];
if ($previous_source) {
if ($previous_source === $source) {
return [];
}
return array_merge($this->getIssueTrace($previous_source), [$node]);
}
return [$node];
}
2020-09-21 01:26:49 +02:00
public function addGraph(self $taint) : void
{
2020-05-22 04:47:58 +02:00
$this->sources += $taint->sources;
$this->sinks += $taint->sinks;
$this->nodes += $taint->nodes;
$this->specialized_calls += $taint->specialized_calls;
foreach ($taint->forward_edges as $key => $map) {
if (!isset($this->forward_edges[$key])) {
$this->forward_edges[$key] = $map;
} else {
$this->forward_edges[$key] += $map;
2019-10-14 02:10:31 +02:00
}
}
2020-05-25 23:10:53 +02:00
foreach ($taint->specializations as $key => $map) {
if (!isset($this->specializations[$key])) {
$this->specializations[$key] = $map;
} else {
$this->specializations[$key] += $map;
}
}
2020-05-22 04:47:58 +02:00
}
public function connectSinksAndSources() : void
{
$visited_source_ids = [];
2020-05-22 05:43:13 +02:00
$sources = $this->sources;
$sinks = $this->sinks;
2020-05-22 04:47:58 +02:00
2020-06-22 08:10:03 +02:00
for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) {
2020-05-22 04:47:58 +02:00
$new_sources = [];
foreach ($sources as $source) {
$source_taints = $source->taints;
\sort($source_taints);
$visited_source_ids[$source->id][implode(',', $source_taints)] = true;
2020-06-16 00:34:56 +02:00
$generated_sources = $this->getSpecializedSources($source);
foreach ($generated_sources as $generated_source) {
$new_sources = array_merge(
$new_sources,
$this->getChildNodes(
$generated_source,
$source_taints,
$sinks,
$visited_source_ids
)
);
}
}
$sources = $new_sources;
}
}
/**
* @param array<string> $source_taints
* @param array<ControlFlowNode> $sinks
* @return array<ControlFlowNode>
2020-06-16 00:34:56 +02:00
*/
private function getChildNodes(
ControlFlowNode $generated_source,
2020-06-16 00:34:56 +02:00
array $source_taints,
array $sinks,
array $visited_source_ids
) : array {
$new_sources = [];
2020-06-22 08:10:03 +02:00
foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) {
$path_type = $path->type;
2020-06-26 01:12:30 +02:00
$added_taints = $path->unescaped_taints ?: [];
$removed_taints = $path->escaped_taints ?: [];
2020-06-19 00:48:19 +02:00
2020-06-16 00:34:56 +02:00
if (!isset($this->nodes[$to_id])) {
continue;
}
$new_taints = \array_unique(
\array_diff(
\array_merge($source_taints, $added_taints),
$removed_taints
2020-06-16 00:34:56 +02:00
)
);
\sort($new_taints);
2019-10-14 02:10:31 +02:00
2020-06-16 00:34:56 +02:00
$destination_node = $this->nodes[$to_id];
if (isset($visited_source_ids[$to_id][implode(',', $new_taints)])) {
continue;
}
2020-06-25 07:32:57 +02:00
if (self::shouldIgnoreFetch($path_type, 'array', $generated_source->path_types)) {
continue;
}
2020-06-25 07:32:57 +02:00
if (self::shouldIgnoreFetch($path_type, 'property', $generated_source->path_types)) {
continue;
2020-06-19 00:48:19 +02:00
}
2020-06-16 00:34:56 +02:00
if (isset($sinks[$to_id])) {
$matching_taints = array_intersect($sinks[$to_id]->taints, $new_taints);
if ($matching_taints && $generated_source->code_location) {
$config = \Psalm\Config::getInstance();
if ($sinks[$to_id]->code_location
&& $config->reportIssueInFile('TaintedInput', $sinks[$to_id]->code_location->file_path)
) {
$issue_location = $sinks[$to_id]->code_location;
} else {
$issue_location = $generated_source->code_location;
}
2020-06-16 00:34:56 +02:00
if (IssueBuffer::accepts(
new TaintedInput(
'Detected tainted ' . implode(', ', $matching_taints),
$issue_location,
$this->getIssueTrace($generated_source),
$this->getPredecessorPath($generated_source)
. ' -> ' . $this->getSuccessorPath($sinks[$to_id])
2020-06-16 00:34:56 +02:00
)
)) {
// fall through
2020-05-22 04:47:58 +02:00
}
2020-06-16 00:34:56 +02:00
continue;
2019-10-14 02:10:31 +02:00
}
2020-06-16 00:34:56 +02:00
}
2019-10-14 02:10:31 +02:00
2020-06-16 00:34:56 +02:00
$new_destination = clone $destination_node;
$new_destination->previous = $generated_source;
$new_destination->taints = $new_taints;
$new_destination->specialized_calls = $generated_source->specialized_calls;
2020-06-19 00:48:19 +02:00
$new_destination->path_types = array_merge($generated_source->path_types, [$path_type]);
2019-10-14 02:10:31 +02:00
2020-06-16 00:34:56 +02:00
$new_sources[$to_id] = $new_destination;
}
2019-10-13 18:34:40 +02:00
2020-06-16 00:34:56 +02:00
return $new_sources;
}
2020-08-23 19:52:31 +02:00
/**
* @param array<string> $previous_path_types
*
* @psalm-pure
*/
2020-06-25 07:32:57 +02:00
private static function shouldIgnoreFetch(
string $path_type,
string $expression_type,
array $previous_path_types
) : bool {
$el = \strlen($expression_type);
if (substr($path_type, 0, $el + 7) === $expression_type . '-fetch-') {
$fetch_nesting = 0;
$previous_path_types = array_reverse($previous_path_types);
foreach ($previous_path_types as $previous_path_type) {
if ($previous_path_type === $expression_type . '-assignment') {
if ($fetch_nesting === 0) {
return false;
}
$fetch_nesting--;
}
if (substr($previous_path_type, 0, $el + 6) === $expression_type . '-fetch') {
$fetch_nesting++;
}
if (substr($previous_path_type, 0, $el + 12) === $expression_type . '-assignment-') {
if ($fetch_nesting > 0) {
$fetch_nesting--;
continue;
}
if (substr($previous_path_type, $el + 12) === substr($path_type, $el + 7)) {
return false;
}
return true;
}
}
}
return false;
}
/** @return array<ControlFlowNode> */
private function getSpecializedSources(ControlFlowNode $source) : array
2020-06-16 00:34:56 +02:00
{
$generated_sources = [];
2020-06-16 00:34:56 +02:00
if (isset($this->forward_edges[$source->id])) {
return [$source];
}
2019-10-14 04:05:16 +02:00
2020-06-16 00:34:56 +02:00
if ($source->specialization_key && isset($this->specialized_calls[$source->specialization_key])) {
$generated_source = clone $source;
$generated_source->specialized_calls[$source->specialization_key]
= $this->specialized_calls[$source->specialization_key];
$generated_source->id = substr($source->id, 0, -strlen($source->specialization_key) - 1);
2019-10-14 04:05:16 +02:00
2020-06-16 00:34:56 +02:00
$generated_sources[] = $generated_source;
} elseif (isset($this->specializations[$source->id])) {
foreach ($this->specializations[$source->id] as $specialization => $_) {
if (!$source->specialized_calls || isset($source->specialized_calls[$specialization])) {
2020-06-16 00:34:56 +02:00
$new_source = clone $source;
2019-10-14 04:05:16 +02:00
2020-06-16 00:34:56 +02:00
$new_source->id = $source->id . '-' . $specialization;
$generated_sources[] = $new_source;
2020-05-22 04:47:58 +02:00
}
2019-10-14 04:05:16 +02:00
}
2020-06-16 00:34:56 +02:00
} else {
foreach ($source->specialized_calls as $key => $map) {
if (isset($map[$source->id]) && isset($this->forward_edges[$source->id . '-' . $key])) {
$new_source = clone $source;
2019-10-13 18:34:40 +02:00
2020-06-16 00:34:56 +02:00
$new_source->id = $source->id . '-' . $key;
$generated_sources[] = $new_source;
}
}
2020-05-22 04:47:58 +02:00
}
2020-06-16 00:34:56 +02:00
return \array_filter(
$generated_sources,
function ($new_source): bool {
2020-06-16 00:34:56 +02:00
return isset($this->forward_edges[$new_source->id]);
}
);
}
}