Skip to content

Commit

Permalink
Add option to dump taint graph (#5080)
Browse files Browse the repository at this point in the history
* Add option to dump taint graph

* Fix types

* Simplify types

Co-authored-by: Matthew Brown <github@muglug.com>
  • Loading branch information
adrienlucas and muglug committed Jan 22, 2021
1 parent d97db04 commit 6f1f680
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 0 deletions.
9 changes: 9 additions & 0 deletions docs/security_analysis/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,12 @@ To generate a SARIF report run Psalm with the `--report` flag and a `.sarif` ext
```bash
psalm --report=results.sarif
```

## Debugging the taint graph

Psalm can output the taint graph using the DOT language. This is useful when expected taints are not detected. To generate a DOT graph run Psalm with the `--dump-taint-graph` flag. For example:

```bash
psalm --taint-analysis --dump-taint-graph=taints.dot
dot -Tsvg -o taints.svg taints.dot
```
17 changes: 17 additions & 0 deletions src/Psalm/Internal/Codebase/DataFlowGraph.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use function strlen;
use function array_reverse;
use function array_sum;
use function array_walk;
use function array_merge;
use function array_keys;

abstract class DataFlowGraph
{
Expand Down Expand Up @@ -134,4 +137,18 @@ public function getEdgeStats() : array

return [$count, \count($origin_counts), \count($destination_counts), $mean];
}

/**
* @psalm-return list<list<string>>
*/
public function summarizeEdges(): array
{
$edges = [];

foreach ($this->forward_edges as $source => $destinations) {
$edges[] = array_merge([$source], array_keys($destinations));
}

return $edges;
}
}
3 changes: 3 additions & 0 deletions src/command_functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ function getPsalmHelpText(): string
--taint-analysis
Run Psalm in taint analysis mode – see https://psalm.dev/docs/security_analysis for more info
--dump-taint-graph=OUTPUT_PATH
Output the taint graph using the DOT language – requires --taint-analysis
Issue baselines:
--set-baseline=PATH
Save all current error level issues to a file, to mark them as info in subsequent runs
Expand Down
16 changes: 16 additions & 0 deletions src/psalm.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
'track-tainted-input',
'taint-analysis',
'security-analysis',
'dump-taint-graph:',
'find-unused-psalm-suppress',
'error-level:',
];
Expand Down Expand Up @@ -282,6 +283,9 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class
|| isset($options['security-analysis'])
|| isset($options['taint-analysis']));

/** @var string|null $dump_taint_graph */
$dump_taint_graph = $options['dump-taint-graph'] ?? null;

if (array_key_exists('v', $options)) {
echo 'Psalm ' . PSALM_VERSION . PHP_EOL;
exit;
Expand Down Expand Up @@ -683,6 +687,18 @@ function (string $arg): bool {
$project_analyzer->findReferencesTo($find_references_to);
}

$flow_graph = $project_analyzer->getCodebase()->taint_flow_graph;
if ($flow_graph !== null && $dump_taint_graph !== null) {
file_put_contents($dump_taint_graph, "digraph Taints {\n\t".
implode("\n\t", array_map(
function (array $edges) {
return '"'.implode('" -> "', $edges).'"';
},
$flow_graph->summarizeEdges()
)) .
"\n}\n");
}

if (isset($options['set-baseline']) && is_string($options['set-baseline'])) {
fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL);

Expand Down
19 changes: 19 additions & 0 deletions tests/EndToEnd/PsalmEndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,25 @@ public function testTaintingWithoutInit(): void
$this->assertSame(1, $result['CODE']);
}

public function testTaintGraphDumping(): void
{
$this->runPsalmInit(1);
$result = $this->runPsalm(
[
'--taint-analysis',
'--dump-taint-graph='.self::$tmpDir.'/taints.dot',
],
self::$tmpDir,
true
);

$this->assertSame(1, $result['CODE']);
$this->assertFileEquals(
__DIR__ . '/../fixtures/expected_taint_graph.dot',
self::$tmpDir.'/taints.dot'
);
}

public function testLegacyConfigWithoutresolveFromConfigFile(): void
{
$this->runPsalmInit(1);
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/expected_taint_graph.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
digraph Taints {
"$_GET:src/FileWithErrors.php:345" -> "$_GET['abc']-src/FileWithErrors.php:345-349"
"$_GET['abc']-src/FileWithErrors.php:345-349" -> "coalesce-src/FileWithErrors.php:345-363"
"$s-src/FileWithErrors.php:109-110" -> "acme\sampleproject\bar"
"$s-src/FileWithErrors.php:162-163" -> "acme\sampleproject\baz"
"$s-src/FileWithErrors.php:215-216" -> "acme\sampleproject\bat"
"$s-src/FileWithErrors.php:269-270" -> "acme\sampleproject\bang"
"acme\sampleproject\bang#1" -> "$s-src/FileWithErrors.php:269-270"
"acme\sampleproject\bar#1" -> "$s-src/FileWithErrors.php:109-110"
"acme\sampleproject\bat#1" -> "$s-src/FileWithErrors.php:215-216"
"acme\sampleproject\baz#1" -> "$s-src/FileWithErrors.php:162-163"
"acme\sampleproject\foo#1" -> "$s-src/FileWithErrors.php:57-58"
"call to echo-src/FileWithErrors.php:335-364" -> "echo#1-src/filewitherrors.php:330"
"coalesce-src/FileWithErrors.php:345-363" -> "call to echo-src/FileWithErrors.php:335-364"
}

0 comments on commit 6f1f680

Please sign in to comment.