Skip to content

Commit 5f0d74e

Browse files
committed
Finalize refactoring
1 parent 8982e90 commit 5f0d74e

9 files changed

+127
-75
lines changed

src/Psalm/Codebase.php

+84-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Psalm;
66

7+
use AssertionError;
78
use Exception;
89
use InvalidArgumentException;
910
use LanguageServerProtocol\Command;
@@ -65,10 +66,11 @@
6566
use Psalm\Type\Atomic\TLiteralInt;
6667
use Psalm\Type\Atomic\TLiteralString;
6768
use Psalm\Type\Atomic\TNamedObject;
68-
use Psalm\Type\TaintKindGroup;
69+
use Psalm\Type\TaintKind;
6970
use Psalm\Type\Union;
7071
use ReflectionProperty;
7172
use ReflectionType;
73+
use RuntimeException;
7274
use UnexpectedValueException;
7375

7476
use function array_combine;
@@ -99,6 +101,7 @@
99101
use function substr;
100102
use function substr_count;
101103

104+
use const PHP_INT_SIZE;
102105
use const PHP_VERSION_ID;
103106

104107
/**
@@ -254,6 +257,42 @@ final class Codebase
254257

255258
public bool $literal_array_key_check = false;
256259

260+
/** @internal */
261+
public int $taint_count = TaintKind::BUILTIN_TAINT_COUNT;
262+
263+
/**
264+
* @var array<string, int>
265+
*/
266+
private array $taint_map = [
267+
'callable' => TaintKind::INPUT_CALLABLE,
268+
'unserialize' => TaintKind::INPUT_UNSERIALIZE,
269+
'include' => TaintKind::INPUT_INCLUDE,
270+
'eval' => TaintKind::INPUT_EVAL,
271+
'ldap' => TaintKind::INPUT_LDAP,
272+
'sql' => TaintKind::INPUT_SQL,
273+
'html' => TaintKind::INPUT_HTML,
274+
'has_quotes' => TaintKind::INPUT_HAS_QUOTES,
275+
'shell' => TaintKind::INPUT_SHELL,
276+
'ssrf' => TaintKind::INPUT_SSRF,
277+
'file' => TaintKind::INPUT_FILE,
278+
'cookie' => TaintKind::INPUT_COOKIE,
279+
'header' => TaintKind::INPUT_HEADER,
280+
'xpath' => TaintKind::INPUT_XPATH,
281+
'sleep' => TaintKind::INPUT_SLEEP,
282+
'extract' => TaintKind::INPUT_EXTRACT,
283+
'user_secret' => TaintKind::USER_SECRET,
284+
'system_secret' => TaintKind::SYSTEM_SECRET,
285+
286+
'input' => TaintKind::ALL_INPUT,
287+
'tainted' => TaintKind::ALL_INPUT,
288+
];
289+
290+
/**
291+
* @internal
292+
* @var array<int, string>
293+
*/
294+
public array $custom_taints = [];
295+
257296
/** @internal */
258297
public function __construct(
259298
public Config $config,
@@ -319,6 +358,48 @@ public function __construct(
319358
$this->loadAnalyzer();
320359
}
321360

361+
/**
362+
* Used to register a taint, or to fetch the ID of an already registered taint by its alias.
363+
*
364+
* @throws AssertionError if no more taint slots are left
365+
* @throws RuntimeException if the passed alias uses some unregistered taint slots
366+
* @param ?int $alias Used to register an alias of one or more pre-existing taints.
367+
*/
368+
public function getOrRegisterTaint(string $taint_type, ?int $alias = null): int
369+
{
370+
if (isset($this->taint_map[$taint_type])) {
371+
return $this->taint_map[$taint_type];
372+
}
373+
if ($alias === null) {
374+
if ($this->taint_count+1 === (PHP_INT_SIZE * 8)) {
375+
if (PHP_INT_SIZE === 8) {
376+
throw new RuntimeException("No more taint slots left, please register fewer taints or use some of the built-in taints!");
377+
}
378+
throw new RuntimeException("No more taint slots left, please switch to a 64-bit build of PHP to get 32 more taint slots, or register fewer taints or use some of the built-in taints!");
379+
}
380+
$id = 1 << ($this->taint_count++);
381+
$this->custom_taints[$id] + $taint_type;
382+
} else {
383+
if ($this->taint_count+1 !== (PHP_INT_SIZE * 8)) {
384+
$mask = (1 << $this->taint_count) - 1;
385+
if ($alias & ~$mask) {
386+
throw new AssertionError("The passed alias $alias uses some not yet registered taint slots!");
387+
}
388+
}
389+
$id = $alias;
390+
}
391+
$this->taint_map[$taint_type] = $id;
392+
return $id;
393+
}
394+
395+
/**
396+
* Used to to fetch the ID of an already registered taint by its alias, or null if no taint is registered for the alias.
397+
*/
398+
public function getTaint(string $taint_type): ?int
399+
{
400+
return $this->taint_map[$taint_type] ?? null;
401+
}
402+
322403
private function loadAnalyzer(): void
323404
{
324405
$this->analyzer = new Analyzer(
@@ -2145,7 +2226,7 @@ public function queueClassLikeForScanning(
21452226
public function addTaintSource(
21462227
Union $expr_type,
21472228
string $taint_id,
2148-
int $taints = TaintKindGroup::ALL_INPUT,
2229+
int $taints = TaintKind::ALL_INPUT,
21492230
?CodeLocation $code_location = null,
21502231
): Union {
21512232
if (!$this->taint_flow_graph) {
@@ -2167,7 +2248,7 @@ public function addTaintSource(
21672248

21682249
public function addTaintSink(
21692250
string $taint_id,
2170-
int $taints = TaintKindGroup::ALL_INPUT,
2251+
int $taints = TaintKind::ALL_INPUT,
21712252
?CodeLocation $code_location = null,
21722253
): void {
21732254
if (!$this->taint_flow_graph) {

src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
2222
use Psalm\Internal\Type\TemplateResult;
2323
use Psalm\Internal\Type\TypeExpander;
24+
use Psalm\Issue\InvalidDocblock;
25+
use Psalm\IssueBuffer;
2426
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
2527
use Psalm\Plugin\EventHandler\Event\AfterFunctionCallAnalysisEvent;
2628
use Psalm\Storage\FunctionLikeStorage;
@@ -39,7 +41,6 @@
3941
use Psalm\Type\Atomic\TNull;
4042
use Psalm\Type\Atomic\TString;
4143
use Psalm\Type\TaintKind;
42-
use Psalm\Type\TaintKindGroup;
4344
use Psalm\Type\Union;
4445
use UnexpectedValueException;
4546

@@ -591,7 +592,15 @@ private static function taintReturnType(
591592

592593
if (!$expanded_type->isNullable()) {
593594
foreach ($expanded_type->getLiteralStrings() as $literal_string) {
594-
$conditionally_removed_taints |= TaintKindGroup::NAME_TO_TAINT[$literal_string->value];
595+
$taint = $codebase->getTaint($literal_string->value);
596+
if ($taint === null) {
597+
IssueBuffer::maybeAdd(new InvalidDocblock(
598+
"Invalid taint name {$literal_string->value} provided",
599+
$function_storage->location,
600+
));
601+
continue;
602+
}
603+
$conditionally_removed_taints |= $taint;
595604
}
596605
}
597606
}

src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
use Throwable;
3535
use UnexpectedValueException;
3636

37-
use function array_filter;
3837
use function count;
3938
use function in_array;
4039
use function strtolower;

src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
2121
use Psalm\Internal\Type\TemplateResult;
2222
use Psalm\Internal\Type\TypeExpander;
23+
use Psalm\Issue\InvalidDocblock;
2324
use Psalm\Issue\NonStaticSelfCall;
2425
use Psalm\Issue\ParentNotFound;
2526
use Psalm\IssueBuffer;
2627
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
2728
use Psalm\Storage\MethodStorage;
2829
use Psalm\Type;
2930
use Psalm\Type\Atomic\TNamedObject;
30-
use Psalm\Type\TaintKindGroup;
3131
use Psalm\Type\Union;
3232

3333
use function count;
@@ -330,7 +330,15 @@ public static function taintReturnType(
330330
);
331331

332332
foreach ($expanded_type->getLiteralStrings() as $literal_string) {
333-
$conditionally_removed_taints |= TaintKindGroup::NAME_TO_TAINT[$literal_string->value];
333+
$taint = $codebase->getTaint($literal_string->value);
334+
if ($taint === null) {
335+
IssueBuffer::maybeAdd(new InvalidDocblock(
336+
"Invalid taint name {$literal_string->value} provided",
337+
$method_storage->location,
338+
));
339+
continue;
340+
}
341+
$conditionally_removed_taints |= $taint;
334342
}
335343
}
336344
}

src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
use Psalm\Type\Atomic\TNonEmptyString;
3535
use Psalm\Type\Atomic\TNull;
3636
use Psalm\Type\Atomic\TString;
37-
use Psalm\Type\TaintKindGroup;
37+
use Psalm\Type\TaintKind;
3838
use Psalm\Type\Union;
3939

4040
use function in_array;
@@ -509,7 +509,7 @@ private static function taintVariable(
509509
|| $var_name === '$_COOKIE'
510510
|| $var_name === '$_REQUEST'
511511
) {
512-
$taints = TaintKindGroup::ALL_INPUT;
512+
$taints = TaintKind::ALL_INPUT;
513513
} else {
514514
$taints = 0;
515515
}

src/Psalm/Internal/Codebase/TaintFlowGraph.php

+15-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Override;
88
use Psalm\CodeLocation;
9+
use Psalm\Codebase;
910
use Psalm\Config;
1011
use Psalm\Internal\Analyzer\ProjectAnalyzer;
1112
use Psalm\Internal\DataFlow\DataFlowNode;
@@ -34,7 +35,6 @@
3435
use Psalm\Progress\Phase;
3536
use Psalm\Progress\Progress;
3637
use Psalm\Type\TaintKind;
37-
use Psalm\Type\TaintKindGroup;
3838

3939
use function count;
4040
use function end;
@@ -215,6 +215,8 @@ public function connectSinksAndSources(Progress $progress): void
215215

216216
$project_analyzer = ProjectAnalyzer::getInstance();
217217

218+
$codebase = $project_analyzer->getCodebase();
219+
218220
// Remove all specializations without an outgoing edge
219221
foreach ($this->specializations as $k => &$map) {
220222
foreach ($map as $kk => $specialized_id) {
@@ -250,6 +252,7 @@ public function connectSinksAndSources(Progress $progress): void
250252
$visited_source_ids,
251253
$config,
252254
$project_analyzer,
255+
$codebase,
253256
);
254257
continue;
255258
}
@@ -275,6 +278,7 @@ public function connectSinksAndSources(Progress $progress): void
275278
$visited_source_ids,
276279
$config,
277280
$project_analyzer,
281+
$codebase,
278282
);
279283

280284
// If this node has first level specializations (=> is first-level & unspecialized),
@@ -299,6 +303,7 @@ public function connectSinksAndSources(Progress $progress): void
299303
$visited_source_ids,
300304
$config,
301305
$project_analyzer,
306+
$codebase,
302307
);
303308
}
304309
} else {
@@ -316,11 +321,12 @@ public function connectSinksAndSources(Progress $progress): void
316321
$visited_source_ids,
317322
$config,
318323
$project_analyzer,
324+
$codebase,
319325
);
320326
}
321327
}
322328
} else {
323-
// Process all descendants
329+
// Process all descendants
324330
foreach ($source->processing_specialized_descendants_of as $map) {
325331
if (isset($map[$source->id])) {
326332
$specialized_id = $map[$source->id];
@@ -339,6 +345,7 @@ public function connectSinksAndSources(Progress $progress): void
339345
$visited_source_ids,
340346
$config,
341347
$project_analyzer,
348+
$codebase,
342349
);
343350
}
344351
}
@@ -365,6 +372,7 @@ private function getChildNodes(
365372
array $visited_source_ids,
366373
Config $config,
367374
ProjectAnalyzer $project_analyzer,
375+
Codebase $codebase,
368376
): void {
369377
foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) {
370378
if (!isset($this->nodes[$to_id])) {
@@ -415,11 +423,12 @@ private function getChildNodes(
415423
$path = $this->getPredecessorPath($generated_source)
416424
. ' -> ' . $this->getSuccessorPath($sink);
417425

418-
foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint => $_) {
419-
if (!($matching_taints & $matching_taint)) {
426+
for ($x = $codebase->taint_count-1; $x >= 0; $x--) {
427+
$t = 1 << $x;
428+
if (!($matching_taints & $t)) {
420429
continue;
421430
}
422-
$issue = match ($matching_taint) {
431+
$issue = match ($t) {
423432
TaintKind::INPUT_CALLABLE => new TaintedCallable(
424433
'Detected tainted text',
425434
$issue_location,
@@ -529,7 +538,7 @@ private function getChildNodes(
529538
$path,
530539
),
531540
default => new TaintedCustom(
532-
'Detected tainted ' . $matching_taint,
541+
'Detected tainted ' . $codebase->custom_taints[$t],
533542
$issue_location,
534543
$issue_trace,
535544
$path,

src/Psalm/Internal/PreloaderList.php

-1
Original file line numberDiff line numberDiff line change
@@ -1755,7 +1755,6 @@ final class PreloaderList {
17551755
\Psalm\Type\MutableUnion::class,
17561756
\Psalm\Type\Reconciler::class,
17571757
\Psalm\Type\TaintKind::class,
1758-
\Psalm\Type\TaintKindGroup::class,
17591758
\Psalm\Type\TypeNode::class,
17601759
\Psalm\Type\TypeVisitor::class,
17611760
\Psalm\Type\Union::class,

src/Psalm/Type/TaintKind.php

+5
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ final class TaintKind
3030
public const INPUT_EXTRACT = (1 << 16);
3131
public const USER_SECRET = (1 << 17);
3232
public const SYSTEM_SECRET = (1 << 18);
33+
34+
public const ALL_INPUT = (1 << 17) - 1;
35+
36+
/** @internal Keep this synced with the above */
37+
public const BUILTIN_TAINT_COUNT = 19;
3338
}

0 commit comments

Comments
 (0)