Skip to content

Commit cd56571

Browse files
authored
Huge performance improvements for taint analysis
2 parents be92afa + caafc3c commit cd56571

File tree

76 files changed

+1053
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1053
-909
lines changed

UPGRADING.md

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
## Changed
44

5+
- [BC] Taints are now *internally* represented by a bitmap (an integer), instead of an array of strings. Users can still use the usual string taint identifiers (including custom ones, which will be automatically registered by Psalm), but internally, the type of `Psalm\Type\TaintKind` taint types is now an integer.
6+
7+
- [BC] The maximum number of usable taint *types* (including both native taints and custom taints) is now equal to 32 on 32-bit systems and 64 on 64-bit systems: this should be enough for the vast majority of usecases, if more taint types are needed, consider merging some taint types or using some native taint types.
8+
9+
- [BC] `Psalm\Plugin\EventHandler\AddTaintsInterface::addTaints` and `Psalm\Plugin\EventHandler\RemoveTaintsInterface::removeTaints` now must return an integer taint instead of an array of strings (see the new [taint documentation](https://psalm.dev/docs/security_analysis/custom_taint_sources/) for more info).
10+
11+
- [BC] The type of the `$taints` parameter of `Psalm\Codebase::addTaintSource` and `Psalm\Codebase::addTaintSink` was changed to an integer
12+
13+
- [BC] Type of property `Psalm\Storage\FunctionLikeParameter::$sinks` changed from `array|null` to `int`
14+
15+
- [BC] Type of property `Psalm\Storage\FunctionLikeStorage::$taint_source_types` changed from `array` to `int`
16+
17+
- [BC] Type of property `Psalm\Storage\FunctionLikeStorage::$added_taints` changed from `array` to `int`
18+
19+
- [BC] Type of property `Psalm\Storage\FunctionLikeStorage::$removed_taints` changed from `array` to `int`
20+
521
- [BC] The `startScanningFiles`, `startAnalyzingFiles`, `startAlteringFiles` of `Psalm\Progress\Progress` and subclasses were removed and replaced with a new `startPhase` method, taking a `Psalm\Progress\Phase` enum case.
622

723
- [BC] The `start` method was removed, use `expand`, instead; the progress is reset to 0 when changing the current phase.

dictionaries/InternalTaintSinkMap.php

+53-53
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,59 @@
55
// This maps internal function names to sink types that we don’t want to end up there
66

77
/**
8-
* @var non-empty-array<string, non-empty-list<list<TaintKind::*>>>
8+
* @var non-empty-array<string, non-empty-list<int-mask-of<TaintKind::*>>>
99
*/
1010
return [
11-
'exec' => [['shell']],
12-
'create_function' => [[], ['eval']],
13-
'file_get_contents' => [['file']],
14-
'file_put_contents' => [['file']],
15-
'fopen' => [['file']],
16-
'unlink' => [['file']],
17-
'copy' => [['file'], ['file']],
18-
'file' => [['file']],
19-
'link' => [['file'], ['file']],
20-
'mkdir' => [['file']],
21-
'move_uploaded_file' => [['file'], ['file']],
22-
'parse_ini_file' => [['file']],
23-
'chown' => [['file']],
24-
'lchown' => [['file']],
25-
'readfile' => [['file']],
26-
'rename' => [['file'], ['file']],
27-
'rmdir' => [['file']],
28-
'header' => [['header']],
29-
'symlink' => [['file']],
30-
'tempnam' => [['file']],
31-
'igbinary_unserialize' => [['unserialize']],
32-
'ldap_search' => [[], ['ldap'], ['ldap']],
33-
'mysqli_query' => [[], ['sql']],
34-
'mysqli::query' => [['sql']],
35-
'mysqli_real_query' => [[], ['sql']],
36-
'mysqli::real_query' => [['sql']],
37-
'mysqli_multi_query' => [[], ['sql']],
38-
'mysqli::multi_query' => [['sql']],
39-
'mysqli_prepare' => [[], ['sql']],
40-
'mysqli::prepare' => [['sql']],
41-
'mysqli_stmt::__construct' => [[], ['sql']],
42-
'mysqli_stmt_prepare' => [[], ['sql']],
43-
'mysqli_stmt::prepare' => [['sql']],
44-
'passthru' => [['shell']],
45-
'pcntl_exec' => [['shell']],
46-
'pg_exec' => [[], ['sql']],
47-
'pg_prepare' => [[], [], ['sql']],
48-
'pg_put_line' => [[], ['sql']],
49-
'pg_query' => [[], ['sql']],
50-
'pg_query_params' => [[], ['sql']],
51-
'pg_send_prepare' => [[], [], ['sql']],
52-
'pg_send_query' => [[], ['sql']],
53-
'pg_send_query_params' => [[], ['sql'], []],
54-
'setcookie' => [['cookie'], ['cookie']],
55-
'shell_exec' => [['shell']],
56-
'system' => [['shell']],
57-
'unserialize' => [['unserialize']],
58-
'popen' => [['shell']],
59-
'proc_open' => [['shell']],
60-
'curl_init' => [['ssrf']],
61-
'curl_setopt' => [[], [], ['ssrf']],
62-
'getimagesize' => [['ssrf']],
11+
'exec' => [TaintKind::INPUT_SHELL],
12+
'create_function' => [0, TaintKind::INPUT_EVAL],
13+
'file_get_contents' => [TaintKind::INPUT_FILE],
14+
'file_put_contents' => [TaintKind::INPUT_FILE],
15+
'fopen' => [TaintKind::INPUT_FILE],
16+
'unlink' => [TaintKind::INPUT_FILE],
17+
'copy' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE],
18+
'file' => [TaintKind::INPUT_FILE],
19+
'link' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE],
20+
'mkdir' => [TaintKind::INPUT_FILE],
21+
'move_uploaded_file' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE],
22+
'parse_ini_file' => [TaintKind::INPUT_FILE],
23+
'chown' => [TaintKind::INPUT_FILE],
24+
'lchown' => [TaintKind::INPUT_FILE],
25+
'readfile' => [TaintKind::INPUT_FILE],
26+
'rename' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE],
27+
'rmdir' => [TaintKind::INPUT_FILE],
28+
'header' => [TaintKind::INPUT_HEADER],
29+
'symlink' => [TaintKind::INPUT_FILE],
30+
'tempnam' => [TaintKind::INPUT_FILE],
31+
'igbinary_unserialize' => [TaintKind::INPUT_UNSERIALIZE],
32+
'ldap_search' => [0, TaintKind::INPUT_LDAP, TaintKind::INPUT_LDAP],
33+
'mysqli_query' => [0, TaintKind::INPUT_SQL],
34+
'mysqli::query' => [TaintKind::INPUT_SQL],
35+
'mysqli_real_query' => [0, TaintKind::INPUT_SQL],
36+
'mysqli::real_query' => [TaintKind::INPUT_SQL],
37+
'mysqli_multi_query' => [0, TaintKind::INPUT_SQL],
38+
'mysqli::multi_query' => [TaintKind::INPUT_SQL],
39+
'mysqli_prepare' => [0, TaintKind::INPUT_SQL],
40+
'mysqli::prepare' => [TaintKind::INPUT_SQL],
41+
'mysqli_stmt::__construct' => [0, TaintKind::INPUT_SQL],
42+
'mysqli_stmt_prepare' => [0, TaintKind::INPUT_SQL],
43+
'mysqli_stmt::prepare' => [TaintKind::INPUT_SQL],
44+
'passthru' => [TaintKind::INPUT_SHELL],
45+
'pcntl_exec' => [TaintKind::INPUT_SHELL],
46+
'pg_exec' => [0, TaintKind::INPUT_SQL],
47+
'pg_prepare' => [0, 0, TaintKind::INPUT_SQL],
48+
'pg_put_line' => [0, TaintKind::INPUT_SQL],
49+
'pg_query' => [0, TaintKind::INPUT_SQL],
50+
'pg_query_params' => [0, TaintKind::INPUT_SQL],
51+
'pg_send_prepare' => [0, 0, TaintKind::INPUT_SQL],
52+
'pg_send_query' => [0, TaintKind::INPUT_SQL],
53+
'pg_send_query_params' => [0, TaintKind::INPUT_SQL, 0],
54+
'setcookie' => [TaintKind::INPUT_COOKIE, TaintKind::INPUT_COOKIE],
55+
'shell_exec' => [TaintKind::INPUT_SHELL],
56+
'system' => [TaintKind::INPUT_SHELL],
57+
'unserialize' => [TaintKind::INPUT_UNSERIALIZE],
58+
'popen' => [TaintKind::INPUT_SHELL],
59+
'proc_open' => [TaintKind::INPUT_SHELL],
60+
'curl_init' => [TaintKind::INPUT_SSRF],
61+
'curl_setopt' => [0, 0, TaintKind::INPUT_SSRF],
62+
'getimagesize' => [TaintKind::INPUT_SSRF],
6363
];

docs/security_analysis/custom_taint_sources.md

+47-6
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,70 @@ For example this plugin treats all variables named `$bad_data` as taint sources.
2626
namespace Psalm\Example\Plugin;
2727

2828
use PhpParser\Node\Expr\Variable;
29+
use Psalm\Codebase;
2930
use Psalm\Plugin\EventHandler\AddTaintsInterface;
3031
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
31-
use Psalm\Type\TaintKindGroup;
32+
use Psalm\Type\TaintKind;
3233

3334
/**
34-
* Add input taints to all variables named 'bad_data'
35+
* Add input taints to all variables named 'bad_data' or 'even_badder_data'.
36+
*
37+
* RemoveTaintsInterface is also available to remove taints.
3538
*/
3639
class TaintBadDataPlugin implements AddTaintsInterface
3740
{
41+
private static int $myCustomTaint;
42+
private static int $myCustomTaintAlias;
43+
/**
44+
* Must be called by the PluginEntryPointInterface (__invoke) of your plugin.
45+
*/
46+
public static function init(Codebase $codebase): void
47+
{
48+
// Register a new custom taint
49+
// The taint name may be used in @psalm-taint-* annotations in the code.
50+
self::$myCustomTaint = $codebase->getOrRegisterTaint("my_custom_taint");
51+
52+
// Register a taint alias that combines multiple pre-registered taint types
53+
// Taint alias names may be used in @psalm-taint-* annotations in the code.
54+
self::$myCustomTaintAlias = $codebase->registerTaintAlias(
55+
"my_custom_taint_alias",
56+
self::$myCustomTaint | TaintKind::ALL_INPUT
57+
);
58+
}
59+
3860
/**
3961
* Called to see what taints should be added
4062
*
41-
* @return list<string>
63+
* @return int A bitmap of taint from the IDs
4264
*/
43-
public static function addTaints(AddRemoveTaintsEvent $event): array
65+
public static function addTaints(AddRemoveTaintsEvent $event): int
4466
{
4567
$expr = $event->getExpr();
4668

4769
if ($expr instanceof Variable && $expr->name === 'bad_data') {
48-
return TaintKindGroup::ALL_INPUT;
70+
return TaintKind::ALL_INPUT;
71+
}
72+
73+
if ($expr instanceof Variable && $expr->name === 'even_badder_data') {
74+
return self::$myCustomTaint;
75+
}
76+
77+
if ($expr instanceof Variable && $expr->name === 'even_badder_data_2') {
78+
return self::$myCustomTaintAlias;
79+
}
80+
81+
if ($expr instanceof Variable && $expr->name === 'secret_even_badder_data_3') {
82+
// Combine taints using |
83+
return self::$myCustomTaintAlias | USER_SECRET;
84+
}
85+
86+
if ($expr instanceof Variable && $expr->name === 'bad_data_but_ok_cookie') {
87+
// Remove taints using & and ~ to negate a taint (group)
88+
return self::$myCustomTaintAlias & ~TaintKind::INPUT_COOKIE;
4989
}
5090

51-
return [];
91+
// No taints
92+
return 0;
5293
}
5394
}
5495
```

examples/TemplateChecker.php

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ private function checkWithViewClass(Context $context, array $stmts): void
178178
$statements_source = new StatementsAnalyzer(
179179
$view_method_analyzer,
180180
new NodeDataProvider(),
181+
false,
181182
);
182183

183184
$statements_source->analyze($pseudo_method_stmts, $context);

examples/plugins/SafeArrayKeyChecker.php

+6-5
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,22 @@
66
use Psalm\Internal\Analyzer\StatementsAnalyzer;
77
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
88
use Psalm\Plugin\EventHandler\RemoveTaintsInterface;
9+
use Psalm\Type\TaintKind;
910

1011
final class SafeArrayKeyChecker implements RemoveTaintsInterface
1112
{
1213
/**
1314
* Called to see what taints should be removed
1415
*
15-
* @return list<string>
16+
* @return int
1617
*/
1718
#[\Override]
18-
public static function removeTaints(AddRemoveTaintsEvent $event): array
19+
public static function removeTaints(AddRemoveTaintsEvent $event): int
1920
{
2021
$item = $event->getExpr();
2122
$statements_analyzer = $event->getStatementsSource();
2223
if (!($item instanceof ArrayItem) || !($statements_analyzer instanceof StatementsAnalyzer)) {
23-
return [];
24+
return 0;
2425
}
2526
$item_key_value = '';
2627
if ($item->key) {
@@ -34,8 +35,8 @@ public static function removeTaints(AddRemoveTaintsEvent $event): array
3435
}
3536

3637
if ($item_key_value === 'safe_key') {
37-
return ['html'];
38+
return TaintKind::INPUT_HTML;
3839
}
39-
return [];
40+
return 0;
4041
}
4142
}

examples/plugins/TaintActiveRecords.php

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
1313
use Psalm\Type\Atomic;
1414
use Psalm\Type\Atomic\TNamedObject;
15+
use Psalm\Type\TaintKind;
1516
use Psalm\Type\TaintKindGroup;
1617
use Psalm\Type\Union;
1718

@@ -27,16 +28,16 @@ final class TaintActiveRecords implements AddTaintsInterface
2728
/**
2829
* Called to see what taints should be added
2930
*
30-
* @return list<string>
31+
* @return int
3132
*/
3233
#[Override]
33-
public static function addTaints(AddRemoveTaintsEvent $event): array
34+
public static function addTaints(AddRemoveTaintsEvent $event): int
3435
{
3536
$expr = $event->getExpr();
3637

3738
// Model properties are accessed by property fetch, so abort here
3839
if ($expr instanceof ArrayItem) {
39-
return [];
40+
return 0;
4041
}
4142

4243
$statements_source = $event->getStatementsSource();
@@ -51,11 +52,11 @@ public static function addTaints(AddRemoveTaintsEvent $event): array
5152
}
5253

5354
if (self::containsActiveRecord($expr_type)) {
54-
return TaintKindGroup::ALL_INPUT;
55+
return TaintKind::ALL_INPUT;
5556
}
5657
} while ($expr = self::getParentNode($expr));
5758

58-
return [];
59+
return 0;
5960
}
6061

6162
/**

psalm-baseline.xml

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="6.x-dev@4258dc813b28c2b03865f34a17ddf072f006b357">
2+
<files psalm-version="dev-master@b513b32ef4a8f5a46c4412133f345182e34db54c">
33
<file src="examples/TemplateChecker.php">
44
<PossiblyUndefinedIntArrayOffset>
55
<code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code>
@@ -911,7 +911,6 @@
911911
<code><![CDATA[$context->calling_method_id]]></code>
912912
<code><![CDATA[$context->calling_method_id]]></code>
913913
<code><![CDATA[$context->calling_method_id]]></code>
914-
<code><![CDATA[$function_param->sinks]]></code>
915914
<code><![CDATA[$function_params]]></code>
916915
<code><![CDATA[$function_params]]></code>
917916
<code><![CDATA[$function_params]]></code>
@@ -1041,7 +1040,6 @@
10411040
<code><![CDATA[$method_id]]></code>
10421041
</ImplicitToStringCast>
10431042
<RiskyTruthyFalsyComparison>
1044-
<code><![CDATA[!$parent_node->specialization_key]]></code>
10451043
<code><![CDATA[$var_id]]></code>
10461044
</RiskyTruthyFalsyComparison>
10471045
</file>
@@ -1621,12 +1619,7 @@
16211619
</file>
16221620
<file src="src/Psalm/Internal/Codebase/TaintFlowGraph.php">
16231621
<RiskyTruthyFalsyComparison>
1624-
<code><![CDATA[$node->specialization_key]]></code>
1625-
<code><![CDATA[$node->unspecialized_id]]></code>
1626-
<code><![CDATA[$path->escaped_taints]]></code>
1627-
<code><![CDATA[$path->unescaped_taints]]></code>
1628-
<code><![CDATA[$source->specialization_key]]></code>
1629-
<code><![CDATA[end($source->path_types)]]></code>
1622+
<code><![CDATA[end($path_types)]]></code>
16301623
</RiskyTruthyFalsyComparison>
16311624
</file>
16321625
<file src="src/Psalm/Internal/Composer.php">
@@ -1635,9 +1628,14 @@
16351628
</RiskyTruthyFalsyComparison>
16361629
</file>
16371630
<file src="src/Psalm/Internal/DataFlow/DataFlowNode.php">
1638-
<RiskyTruthyFalsyComparison>
1639-
<code><![CDATA[$specialization_key]]></code>
1640-
</RiskyTruthyFalsyComparison>
1631+
<RedundantPropertyInitializationCheck>
1632+
<code><![CDATA[new self('closure-use', null, null, 'closure use')]]></code>
1633+
<code><![CDATA[new self('unknown-origin', null, null, 'unknown origin')]]></code>
1634+
<code><![CDATA[new self('variable-use', null, null, 'variable use')]]></code>
1635+
<code><![CDATA[self::$forClosureUse]]></code>
1636+
<code><![CDATA[self::$forUnknownOrigin]]></code>
1637+
<code><![CDATA[self::$forVariableUse]]></code>
1638+
</RedundantPropertyInitializationCheck>
16411639
</file>
16421640
<file src="src/Psalm/Internal/Diff/ClassStatementsDiffer.php">
16431641
<ImplicitToStringCast>

0 commit comments

Comments
 (0)