2019-08-04 16:37:36 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Psalm\Internal\Codebase;
|
|
|
|
|
|
|
|
use Psalm\CodeLocation;
|
|
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
2019-08-14 06:47:57 +02:00
|
|
|
use Psalm\Internal\Taint\Sink;
|
|
|
|
use Psalm\Internal\Taint\Source;
|
|
|
|
use Psalm\Internal\Taint\Taintable;
|
2019-08-04 16:37:36 +02:00
|
|
|
use Psalm\IssueBuffer;
|
|
|
|
use Psalm\Issue\TaintedInput;
|
|
|
|
use function array_merge;
|
|
|
|
use function array_merge_recursive;
|
|
|
|
use function strtolower;
|
|
|
|
use UnexpectedValueException;
|
|
|
|
|
|
|
|
class Taint
|
|
|
|
{
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @var array<string, ?Sink>
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
|
|
|
private $new_sinks = [];
|
|
|
|
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @var array<string, ?Source>
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
|
|
|
private $new_sources = [];
|
|
|
|
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @var array<string, ?Sink>
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
2019-08-06 20:27:21 +02:00
|
|
|
private static $previous_sinks = [];
|
2019-08-04 16:37:36 +02:00
|
|
|
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @var array<string, ?Source>
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
2019-08-06 20:27:21 +02:00
|
|
|
private static $previous_sources = [];
|
2019-08-04 16:37:36 +02:00
|
|
|
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @var array<string, ?Sink>
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
2019-08-06 20:27:21 +02:00
|
|
|
private static $archived_sinks = [];
|
2019-08-04 16:37:36 +02:00
|
|
|
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @var array<string, ?Source>
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
2019-08-06 20:27:21 +02:00
|
|
|
private static $archived_sources = [];
|
2019-08-04 16:37:36 +02:00
|
|
|
|
2019-08-06 00:33:33 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, array<string>>
|
|
|
|
*/
|
|
|
|
private $specializations = [];
|
|
|
|
|
2019-08-06 20:27:21 +02:00
|
|
|
public function __construct()
|
|
|
|
{
|
|
|
|
self::$previous_sinks = [];
|
|
|
|
self::$previous_sources = [];
|
|
|
|
self::$archived_sinks = [];
|
|
|
|
self::$archived_sources = [];
|
|
|
|
}
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
public function hasExistingSink(Taintable $sink) : ?Sink
|
2019-08-04 16:37:36 +02:00
|
|
|
{
|
2019-08-14 06:47:57 +02:00
|
|
|
return self::$archived_sinks[$sink->id] ?? null;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
public function hasExistingSource(Taintable $source) : ?Source
|
2019-08-06 00:33:33 +02:00
|
|
|
{
|
2019-08-06 20:27:21 +02:00
|
|
|
return self::$archived_sources[$source->id] ?? null;
|
2019-08-06 00:33:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ?array<string> $suffixes
|
|
|
|
*/
|
2019-08-14 06:47:57 +02:00
|
|
|
public function hasPreviousSink(Sink $source, ?array &$suffixes = null) : ?Sink
|
2019-08-04 16:37:36 +02:00
|
|
|
{
|
2019-08-06 00:33:33 +02:00
|
|
|
if (isset($this->specializations[$source->id])) {
|
|
|
|
$suffixes = $this->specializations[$source->id];
|
|
|
|
|
|
|
|
foreach ($suffixes as $suffix) {
|
2019-08-06 20:27:21 +02:00
|
|
|
if (isset(self::$previous_sinks[$source->id . '-' . $suffix])) {
|
2019-08-13 05:16:05 +02:00
|
|
|
return self::$previous_sinks[$source->id . '-' . $suffix];
|
2019-08-06 00:33:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-13 05:16:05 +02:00
|
|
|
return null;
|
2019-08-06 00:33:33 +02:00
|
|
|
}
|
|
|
|
|
2019-08-13 05:16:05 +02:00
|
|
|
return self::$previous_sinks[$source->id] ?? null;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-06 00:33:33 +02:00
|
|
|
/**
|
|
|
|
* @param ?array<string> $suffixes
|
|
|
|
*/
|
2019-08-14 06:47:57 +02:00
|
|
|
public function hasPreviousSource(Source $source, ?array &$suffixes = null) : ?Source
|
2019-08-04 16:37:36 +02:00
|
|
|
{
|
2019-08-06 00:33:33 +02:00
|
|
|
if (isset($this->specializations[$source->id])) {
|
|
|
|
$suffixes = $this->specializations[$source->id];
|
|
|
|
|
|
|
|
foreach ($suffixes as $suffix) {
|
2019-08-06 20:27:21 +02:00
|
|
|
if (isset(self::$previous_sources[$source->id . '-' . $suffix])) {
|
2019-08-13 05:16:05 +02:00
|
|
|
return self::$previous_sources[$source->id . '-' . $suffix];
|
2019-08-06 00:33:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-13 05:16:05 +02:00
|
|
|
return null;
|
2019-08-06 00:33:33 +02:00
|
|
|
}
|
|
|
|
|
2019-08-13 05:16:05 +02:00
|
|
|
return self::$previous_sources[$source->id] ?? null;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-06 00:33:33 +02:00
|
|
|
public function addSpecialization(string $base_id, string $suffix) : void
|
2019-08-04 16:37:36 +02:00
|
|
|
{
|
2019-08-06 00:33:33 +02:00
|
|
|
if (isset($this->specializations[$base_id])) {
|
|
|
|
if (!\in_array($suffix, $this->specializations)) {
|
|
|
|
$this->specializations[$base_id][] = $suffix;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->specializations[$base_id] = [$suffix];
|
|
|
|
}
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-08-14 06:47:57 +02:00
|
|
|
* @param array<Source> $sources
|
2019-08-04 16:37:36 +02:00
|
|
|
*/
|
|
|
|
public function addSources(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
2019-08-14 06:47:57 +02:00
|
|
|
array $sources
|
2019-08-04 16:37:36 +02:00
|
|
|
) : void {
|
|
|
|
foreach ($sources as $source) {
|
|
|
|
if ($this->hasExistingSource($source)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
if (($existing_sink = $this->hasExistingSink($source)) && $source->code_location) {
|
2019-08-04 16:37:36 +02:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new TaintedInput(
|
2019-08-14 06:47:57 +02:00
|
|
|
'in path ' . $this->getPredecessorPath($source)
|
|
|
|
. ' out path ' . $this->getSuccessorPath($existing_sink),
|
|
|
|
$source->code_location
|
2019-08-04 16:37:36 +02:00
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
$this->new_sources[$source->id] = $source;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array<Sink> $sinks
|
|
|
|
*/
|
|
|
|
public function addSinks(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
array $sinks
|
|
|
|
) : void {
|
|
|
|
foreach ($sinks as $sink) {
|
|
|
|
if ($this->hasExistingSink($sink)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (($existing_source = $this->hasExistingSource($sink)) && $sink->code_location) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new TaintedInput(
|
|
|
|
'in path ' . $this->getPredecessorPath($existing_source)
|
|
|
|
. ' out path ' . $this->getSuccessorPath($sink),
|
|
|
|
$sink->code_location
|
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->new_sinks[$sink->id] = $sink;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-06 23:29:44 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool> $visited_paths
|
|
|
|
*/
|
2019-08-14 06:47:57 +02:00
|
|
|
public function getPredecessorPath(Source $source, array $visited_paths = []) : string
|
2019-08-04 16:37:36 +02:00
|
|
|
{
|
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->getQuickSummary();
|
2019-08-14 15:52:59 +02:00
|
|
|
}
|
2019-08-06 23:29:44 +02:00
|
|
|
|
2019-08-14 15:52:59 +02:00
|
|
|
if (isset($visited_paths[$source->id])) {
|
|
|
|
return '';
|
2019-08-07 00:56:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-14 15:52:59 +02:00
|
|
|
$visited_paths[$source->id] = true;
|
|
|
|
|
2019-08-07 00:56:36 +02:00
|
|
|
$source_descriptor = $source->id . ($location_summary ? ' (' . $location_summary . ')' : '');
|
2019-08-04 16:37:36 +02:00
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
$previous_source = $source->parents[0] ?? null;
|
|
|
|
|
|
|
|
if ($previous_source) {
|
2019-08-04 16:37:36 +02:00
|
|
|
if ($previous_source === $source) {
|
2019-08-07 00:56:36 +02:00
|
|
|
return '';
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-06 23:29:44 +02:00
|
|
|
return $this->getPredecessorPath($previous_source, $visited_paths) . ' -> ' . $source_descriptor;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $source_descriptor;
|
|
|
|
}
|
|
|
|
|
2019-08-06 23:29:44 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, bool> $visited_paths
|
|
|
|
*/
|
2019-08-14 06:47:57 +02:00
|
|
|
public function getSuccessorPath(Sink $sink, array $visited_paths = []) : string
|
2019-08-04 16:37:36 +02:00
|
|
|
{
|
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->getQuickSummary();
|
2019-08-14 15:52:59 +02:00
|
|
|
}
|
2019-08-06 23:29:44 +02:00
|
|
|
|
2019-08-14 15:52:59 +02:00
|
|
|
if (isset($visited_paths[$sink->id])) {
|
|
|
|
return '';
|
2019-08-07 00:56:36 +02:00
|
|
|
}
|
2019-08-06 23:29:44 +02:00
|
|
|
|
2019-08-14 15:52:59 +02:00
|
|
|
$visited_paths[$sink->id] = true;
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
$sink_descriptor = $sink->id . ($location_summary ? ' (' . $location_summary . ')' : '');
|
2019-08-04 16:37:36 +02:00
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
$next_sink = $sink->children[0] ?? null;
|
|
|
|
|
|
|
|
if ($next_sink) {
|
|
|
|
if ($next_sink === $sink) {
|
2019-08-07 00:56:36 +02:00
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink, $visited_paths);
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-14 06:47:57 +02:00
|
|
|
return $sink_descriptor;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function hasNewSinksAndSources() : bool
|
|
|
|
{
|
2019-08-06 00:33:33 +02:00
|
|
|
return $this->new_sinks || $this->new_sources;
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function addThreadData(self $taint) : void
|
|
|
|
{
|
|
|
|
$this->new_sinks = array_merge(
|
|
|
|
$this->new_sinks,
|
|
|
|
$taint->new_sinks
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->new_sources = array_merge(
|
|
|
|
$this->new_sources,
|
|
|
|
$taint->new_sources
|
|
|
|
);
|
2019-08-06 00:33:33 +02:00
|
|
|
|
|
|
|
foreach ($taint->specializations as $id => $specializations) {
|
|
|
|
if (!isset($this->specializations[$id])) {
|
|
|
|
$this->specializations[$id] = $specializations;
|
|
|
|
} else {
|
|
|
|
$this->specializations[$id] = \array_unique(
|
|
|
|
array_merge($this->specializations[$id], $specializations)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2019-08-04 16:37:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function clearNewSinksAndSources() : void
|
|
|
|
{
|
2019-08-06 20:27:21 +02:00
|
|
|
self::$archived_sinks = array_merge(
|
|
|
|
self::$archived_sinks,
|
2019-08-04 16:37:36 +02:00
|
|
|
$this->new_sinks
|
|
|
|
);
|
|
|
|
|
2019-08-06 20:27:21 +02:00
|
|
|
self::$previous_sinks = $this->new_sinks;
|
2019-08-04 16:37:36 +02:00
|
|
|
|
|
|
|
$this->new_sinks = [];
|
|
|
|
|
2019-08-06 20:27:21 +02:00
|
|
|
self::$archived_sources = array_merge(
|
|
|
|
self::$archived_sources,
|
2019-08-04 16:37:36 +02:00
|
|
|
$this->new_sources
|
|
|
|
);
|
|
|
|
|
2019-08-06 20:27:21 +02:00
|
|
|
self::$previous_sources = $this->new_sources;
|
2019-08-04 16:37:36 +02:00
|
|
|
|
|
|
|
$this->new_sources = [];
|
|
|
|
}
|
|
|
|
}
|