/src/Psalm/Internal/Codebase/TaintFlowGraph.php

https://github.com/vimeo/psalm · PHP · 322 lines · 237 code · 69 blank · 16 comment · 36 complexity · 13019b90730aeefc9a51656f065c2946 MD5 · raw file

  1. <?php
  2. namespace Psalm\Internal\Codebase;
  3. use Psalm\CodeLocation;
  4. use Psalm\Internal\ControlFlow\Path;
  5. use Psalm\Internal\ControlFlow\TaintSink;
  6. use Psalm\Internal\ControlFlow\TaintSource;
  7. use Psalm\Internal\ControlFlow\ControlFlowNode;
  8. use Psalm\IssueBuffer;
  9. use Psalm\Issue\TaintedInput;
  10. use function array_merge;
  11. use function count;
  12. use function implode;
  13. use function substr;
  14. use function strlen;
  15. use function array_intersect;
  16. use function array_reverse;
  17. class TaintFlowGraph extends ControlFlowGraph
  18. {
  19. /** @var array<string, TaintSource> */
  20. private $sources = [];
  21. /** @var array<string, ControlFlowNode> */
  22. private $nodes = [];
  23. /** @var array<string, TaintSink> */
  24. private $sinks = [];
  25. /** @var array<string, array<string, true>> */
  26. private $specialized_calls = [];
  27. /** @var array<string, array<string, true>> */
  28. private $specializations = [];
  29. public function addNode(ControlFlowNode $node) : void
  30. {
  31. $this->nodes[$node->id] = $node;
  32. if ($node->unspecialized_id && $node->specialization_key) {
  33. $this->specialized_calls[$node->specialization_key][$node->unspecialized_id] = true;
  34. $this->specializations[$node->unspecialized_id][$node->specialization_key] = true;
  35. }
  36. }
  37. public function addSource(TaintSource $node) : void
  38. {
  39. $this->sources[$node->id] = $node;
  40. }
  41. public function addSink(TaintSink $node) : void
  42. {
  43. $this->sinks[$node->id] = $node;
  44. // in the rare case the sink is the _next_ node, this is necessary
  45. $this->nodes[$node->id] = $node;
  46. }
  47. public function addGraph(self $taint) : void
  48. {
  49. $this->sources += $taint->sources;
  50. $this->sinks += $taint->sinks;
  51. $this->nodes += $taint->nodes;
  52. $this->specialized_calls += $taint->specialized_calls;
  53. foreach ($taint->forward_edges as $key => $map) {
  54. if (!isset($this->forward_edges[$key])) {
  55. $this->forward_edges[$key] = $map;
  56. } else {
  57. $this->forward_edges[$key] += $map;
  58. }
  59. }
  60. foreach ($taint->specializations as $key => $map) {
  61. if (!isset($this->specializations[$key])) {
  62. $this->specializations[$key] = $map;
  63. } else {
  64. $this->specializations[$key] += $map;
  65. }
  66. }
  67. }
  68. public function getPredecessorPath(ControlFlowNode $source) : string
  69. {
  70. $location_summary = '';
  71. if ($source->code_location) {
  72. $location_summary = $source->code_location->getShortSummary();
  73. }
  74. $source_descriptor = $source->label . ($location_summary ? ' (' . $location_summary . ')' : '');
  75. $previous_source = $source->previous;
  76. if ($previous_source) {
  77. if ($previous_source === $source) {
  78. return '';
  79. }
  80. return $this->getPredecessorPath($previous_source) . ' -> ' . $source_descriptor;
  81. }
  82. return $source_descriptor;
  83. }
  84. public function getSuccessorPath(ControlFlowNode $sink) : string
  85. {
  86. $location_summary = '';
  87. if ($sink->code_location) {
  88. $location_summary = $sink->code_location->getShortSummary();
  89. }
  90. $sink_descriptor = $sink->label . ($location_summary ? ' (' . $location_summary . ')' : '');
  91. $next_sink = $sink->previous;
  92. if ($next_sink) {
  93. if ($next_sink === $sink) {
  94. return '';
  95. }
  96. return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink);
  97. }
  98. return $sink_descriptor;
  99. }
  100. /**
  101. * @return list<array{location: ?CodeLocation, label: string, entry_path_type: string}>
  102. */
  103. public function getIssueTrace(ControlFlowNode $source) : array
  104. {
  105. $previous_source = $source->previous;
  106. $node = [
  107. 'location' => $source->code_location,
  108. 'label' => $source->label,
  109. 'entry_path_type' => \end($source->path_types) ?: ''
  110. ];
  111. if ($previous_source) {
  112. if ($previous_source === $source) {
  113. return [];
  114. }
  115. return array_merge($this->getIssueTrace($previous_source), [$node]);
  116. }
  117. return [$node];
  118. }
  119. public function connectSinksAndSources() : void
  120. {
  121. $visited_source_ids = [];
  122. $sources = $this->sources;
  123. $sinks = $this->sinks;
  124. for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) {
  125. $new_sources = [];
  126. foreach ($sources as $source) {
  127. $source_taints = $source->taints;
  128. \sort($source_taints);
  129. $visited_source_ids[$source->id][implode(',', $source_taints)] = true;
  130. $generated_sources = $this->getSpecializedSources($source);
  131. foreach ($generated_sources as $generated_source) {
  132. $new_sources = array_merge(
  133. $new_sources,
  134. $this->getChildNodes(
  135. $generated_source,
  136. $source_taints,
  137. $sinks,
  138. $visited_source_ids
  139. )
  140. );
  141. }
  142. }
  143. $sources = $new_sources;
  144. }
  145. }
  146. /**
  147. * @param array<string> $source_taints
  148. * @param array<ControlFlowNode> $sinks
  149. * @return array<ControlFlowNode>
  150. */
  151. private function getChildNodes(
  152. ControlFlowNode $generated_source,
  153. array $source_taints,
  154. array $sinks,
  155. array $visited_source_ids
  156. ) : array {
  157. $new_sources = [];
  158. foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) {
  159. $path_type = $path->type;
  160. $added_taints = $path->unescaped_taints ?: [];
  161. $removed_taints = $path->escaped_taints ?: [];
  162. if (!isset($this->nodes[$to_id])) {
  163. continue;
  164. }
  165. $new_taints = \array_unique(
  166. \array_diff(
  167. \array_merge($source_taints, $added_taints),
  168. $removed_taints
  169. )
  170. );
  171. \sort($new_taints);
  172. $destination_node = $this->nodes[$to_id];
  173. if (isset($visited_source_ids[$to_id][implode(',', $new_taints)])) {
  174. continue;
  175. }
  176. if (self::shouldIgnoreFetch($path_type, 'array', $generated_source->path_types)) {
  177. continue;
  178. }
  179. if (self::shouldIgnoreFetch($path_type, 'property', $generated_source->path_types)) {
  180. continue;
  181. }
  182. if (isset($sinks[$to_id])) {
  183. $matching_taints = array_intersect($sinks[$to_id]->taints, $new_taints);
  184. if ($matching_taints && $generated_source->code_location) {
  185. $config = \Psalm\Config::getInstance();
  186. if ($sinks[$to_id]->code_location
  187. && $config->reportIssueInFile('TaintedInput', $sinks[$to_id]->code_location->file_path)
  188. ) {
  189. $issue_location = $sinks[$to_id]->code_location;
  190. } else {
  191. $issue_location = $generated_source->code_location;
  192. }
  193. if (IssueBuffer::accepts(
  194. new TaintedInput(
  195. 'Detected tainted ' . implode(', ', $matching_taints),
  196. $issue_location,
  197. $this->getIssueTrace($generated_source),
  198. $this->getPredecessorPath($generated_source)
  199. . ' -> ' . $this->getSuccessorPath($sinks[$to_id])
  200. )
  201. )) {
  202. // fall through
  203. }
  204. continue;
  205. }
  206. }
  207. $new_destination = clone $destination_node;
  208. $new_destination->previous = $generated_source;
  209. $new_destination->taints = $new_taints;
  210. $new_destination->specialized_calls = $generated_source->specialized_calls;
  211. $new_destination->path_types = array_merge($generated_source->path_types, [$path_type]);
  212. $new_sources[$to_id] = $new_destination;
  213. }
  214. return $new_sources;
  215. }
  216. /** @return array<ControlFlowNode> */
  217. private function getSpecializedSources(ControlFlowNode $source) : array
  218. {
  219. $generated_sources = [];
  220. if (isset($this->forward_edges[$source->id])) {
  221. return [$source];
  222. }
  223. if ($source->specialization_key && isset($this->specialized_calls[$source->specialization_key])) {
  224. $generated_source = clone $source;
  225. $generated_source->specialized_calls[$source->specialization_key]
  226. = $this->specialized_calls[$source->specialization_key];
  227. $generated_source->id = substr($source->id, 0, -strlen($source->specialization_key) - 1);
  228. $generated_sources[] = $generated_source;
  229. } elseif (isset($this->specializations[$source->id])) {
  230. foreach ($this->specializations[$source->id] as $specialization => $_) {
  231. if (!$source->specialized_calls || isset($source->specialized_calls[$specialization])) {
  232. $new_source = clone $source;
  233. $new_source->id = $source->id . '-' . $specialization;
  234. $generated_sources[] = $new_source;
  235. }
  236. }
  237. } else {
  238. foreach ($source->specialized_calls as $key => $map) {
  239. if (isset($map[$source->id]) && isset($this->forward_edges[$source->id . '-' . $key])) {
  240. $new_source = clone $source;
  241. $new_source->id = $source->id . '-' . $key;
  242. $generated_sources[] = $new_source;
  243. }
  244. }
  245. }
  246. return \array_filter(
  247. $generated_sources,
  248. function ($new_source): bool {
  249. return isset($this->forward_edges[$new_source->id]);
  250. }
  251. );
  252. }
  253. }