diff --git a/docs/security_analysis/custom_taint_sources.md b/docs/security_analysis/custom_taint_sources.md index d3bdb0a3205..7b0684b433f 100644 --- a/docs/security_analysis/custom_taint_sources.md +++ b/docs/security_analysis/custom_taint_sources.md @@ -48,6 +48,7 @@ class BadSqlTainter implements AfterExpressionAnalysisInterface $expr = $event->getExpr(); $statements_source = $event->getStatementsSource(); $codebase = $event->getCodebase(); + $taint_type_registry = $codebase->config->taint_type_registry; if ($expr instanceof PhpParser\Node\Expr\Variable && $expr->name === 'bad_data' ) { @@ -63,7 +64,7 @@ class BadSqlTainter implements AfterExpressionAnalysisInterface $codebase->addTaintSource( $expr_type, $expr_identifier, - TaintKindGroup::ALL_INPUT, + $taint_type_registry->resolveGroup(TaintKindGroup::GROUP_INPUT), new CodeLocation($statements_source, $expr) ); } diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index a02f61403f6..13404a3c4d5 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -2423,7 +2423,7 @@ public function queueClassLikeForScanning( public function addTaintSource( Union $expr_type, string $taint_id, - array $taints = TaintKindGroup::ALL_INPUT, + array $taints = null, ?CodeLocation $code_location = null ): Union { if (!$this->taint_flow_graph) { @@ -2435,7 +2435,7 @@ public function addTaintSource( $taint_id, $code_location, null, - $taints, + $taints ?? $this->config->taint_type_registry->resolveGroup(TaintKindGroup::GROUP_INPUT), ); $this->taint_flow_graph->addSource($source); @@ -2449,7 +2449,7 @@ public function addTaintSource( */ public function addTaintSink( string $taint_id, - array $taints = TaintKindGroup::ALL_INPUT, + array $taints = null, ?CodeLocation $code_location = null ): void { if (!$this->taint_flow_graph) { @@ -2461,7 +2461,7 @@ public function addTaintSink( $taint_id, $code_location, null, - $taints, + $taints ?? $this->config->taint_type_registry->resolveGroup(TaintKindGroup::GROUP_INPUT), ); $this->taint_flow_graph->addSink($sink); diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index a4d4fe35e24..c7fc4efc9b4 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -35,6 +35,7 @@ use Psalm\Issue\FunctionIssue; use Psalm\Issue\MethodIssue; use Psalm\Issue\PropertyIssue; +use Psalm\Issue\TaintTypeRegistry; use Psalm\Issue\VariableIssue; use Psalm\Plugin\PluginEntryPointInterface; use Psalm\Plugin\PluginFileExtensionsInterface; @@ -727,6 +728,11 @@ class Config /** @var list */ public array $config_warnings = []; + /** + * @readonly + */ + public TaintTypeRegistry $taint_type_registry; + /** @internal */ protected function __construct() { @@ -735,6 +741,7 @@ protected function __construct() $this->universal_object_crates = [ strtolower(stdClass::class), ]; + $this->taint_type_registry = new TaintTypeRegistry(); } /** diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php index 48791659e47..abb80f89b66 100644 --- a/src/Psalm/Config/IssueHandler.php +++ b/src/Psalm/Config/IssueHandler.php @@ -175,7 +175,9 @@ public static function getAllIssueTypes(): array && $issue_name !== 'ParseError' && $issue_name !== 'PluginIssue' && $issue_name !== 'MixedIssue' - && $issue_name !== 'MixedIssueTrait', + && $issue_name !== 'MixedIssueTrait' + && $issue_name !== 'TaintTypeFactory' + && $issue_name !== 'TaintTypeRegistry', ); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 324dd7b30b0..7b17b4f42ac 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -516,13 +516,14 @@ private static function taintVariable( || $var_name === '$_REQUEST' ) { $taint_location = new CodeLocation($statements_analyzer->getSource(), $stmt); + $taint_type_registry = Config::getInstance()->taint_type_registry; $server_taint_source = new TaintSource( $var_name . ':' . $taint_location->file_name . ':' . $taint_location->raw_file_start, $var_name, null, null, - TaintKindGroup::ALL_INPUT, + $taint_type_registry->resolveGroup(TaintKindGroup::GROUP_INPUT), ); $statements_analyzer->data_flow_graph->addSource($server_taint_source); diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index bb7cd993879..51b55f46867 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -8,24 +8,8 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\DataFlow\TaintSource; -use Psalm\Issue\TaintedCallable; -use Psalm\Issue\TaintedCookie; -use Psalm\Issue\TaintedCustom; -use Psalm\Issue\TaintedEval; -use Psalm\Issue\TaintedFile; -use Psalm\Issue\TaintedHeader; -use Psalm\Issue\TaintedHtml; -use Psalm\Issue\TaintedInclude; -use Psalm\Issue\TaintedLdap; -use Psalm\Issue\TaintedSSRF; -use Psalm\Issue\TaintedShell; -use Psalm\Issue\TaintedSql; -use Psalm\Issue\TaintedSystemSecret; -use Psalm\Issue\TaintedTextWithQuotes; -use Psalm\Issue\TaintedUnserialize; -use Psalm\Issue\TaintedUserSecret; +use Psalm\Issue\TaintTypeFactory; use Psalm\IssueBuffer; -use Psalm\Type\TaintKind; use function array_diff; use function array_filter; @@ -298,6 +282,7 @@ private function getChildNodes( if (isset($sinks[$to_id])) { $matching_taints = array_intersect($sinks[$to_id]->taints, $new_taints); + $matching_taints = array_filter($matching_taints); if ($matching_taints && $generated_source->code_location) { if ($sinks[$to_id]->code_location @@ -312,152 +297,15 @@ private function getChildNodes( $path = $this->getPredecessorPath($generated_source) . ' -> ' . $this->getSuccessorPath($sinks[$to_id]); - foreach ($matching_taints as $matching_taint) { - switch ($matching_taint) { - case TaintKind::INPUT_CALLABLE: - $issue = new TaintedCallable( - 'Detected tainted text', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_UNSERIALIZE: - $issue = new TaintedUnserialize( - 'Detected tainted code passed to unserialize or similar', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_INCLUDE: - $issue = new TaintedInclude( - 'Detected tainted code passed to include or similar', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_EVAL: - $issue = new TaintedEval( - 'Detected tainted code passed to eval or similar', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_SQL: - $issue = new TaintedSql( - 'Detected tainted SQL', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_HTML: - $issue = new TaintedHtml( - 'Detected tainted HTML', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_HAS_QUOTES: - $issue = new TaintedTextWithQuotes( - 'Detected tainted text with possible quotes', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_SHELL: - $issue = new TaintedShell( - 'Detected tainted shell code', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::USER_SECRET: - $issue = new TaintedUserSecret( - 'Detected tainted user secret leaking', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::SYSTEM_SECRET: - $issue = new TaintedSystemSecret( - 'Detected tainted system secret leaking', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_SSRF: - $issue = new TaintedSSRF( - 'Detected tainted network request', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_LDAP: - $issue = new TaintedLdap( - 'Detected tainted LDAP request', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_COOKIE: - $issue = new TaintedCookie( - 'Detected tainted cookie', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_FILE: - $issue = new TaintedFile( - 'Detected tainted file handling', - $issue_location, - $issue_trace, - $path, - ); - break; - - case TaintKind::INPUT_HEADER: - $issue = new TaintedHeader( - 'Detected tainted header', - $issue_location, - $issue_trace, - $path, - ); - break; - - default: - $issue = new TaintedCustom( - 'Detected tainted ' . $matching_taint, - $issue_location, - $issue_trace, - $path, - ); - } + $taint_type_factory = new TaintTypeFactory($config->taint_type_registry); + foreach ($matching_taints as $matching_taint) { + $issue = $taint_type_factory->create( + $matching_taint, + $issue_location, + $issue_trace, + $path, + ); IssueBuffer::maybeAdd($issue); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 270529ee306..dd687a429e8 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -352,12 +352,10 @@ public static function addDocblockInfo( } } + $input_taint_types = $codebase->config->taint_type_registry->resolveGroup(TaintKindGroup::GROUP_INPUT); foreach ($docblock_info->taint_source_types as $taint_source_type) { if ($taint_source_type === 'input') { - $storage->taint_source_types = array_merge( - $storage->taint_source_types, - TaintKindGroup::ALL_INPUT, - ); + $storage->taint_source_types = array_merge($storage->taint_source_types, $input_taint_types); } else { $storage->taint_source_types[] = $taint_source_type; } diff --git a/src/Psalm/Issue/TaintTypeFactory.php b/src/Psalm/Issue/TaintTypeFactory.php new file mode 100644 index 00000000000..4a3918b708e --- /dev/null +++ b/src/Psalm/Issue/TaintTypeFactory.php @@ -0,0 +1,32 @@ +registry = $registry; + } + + /** + * @param non-empty-string $type + */ + public function create( + string $type, + CodeLocation $code_location, + array $journey, + string $journey_text + ): CodeIssue { + /** @var TaintedInput $class_name */ + $class_name = $this->registry->getType($type) ?? $this->registry->getDefaultType(); + return new $class_name($class_name::MESSAGE, $code_location, $journey, $journey_text); + } +} diff --git a/src/Psalm/Issue/TaintTypeRegistry.php b/src/Psalm/Issue/TaintTypeRegistry.php new file mode 100644 index 00000000000..c11a5590dc6 --- /dev/null +++ b/src/Psalm/Issue/TaintTypeRegistry.php @@ -0,0 +1,162 @@ +> + */ + private array $taint_types = [ + TaintKind::INPUT_CALLABLE => TaintedCallable::class, + TaintKind::INPUT_COOKIE => TaintedCookie::class, + TaintKind::INPUT_EVAL => TaintedEval::class, + TaintKind::INPUT_FILE => TaintedFile::class, + TaintKind::INPUT_HAS_QUOTES => TaintedTextWithQuotes::class, + TaintKind::INPUT_HEADER => TaintedHeader::class, + TaintKind::INPUT_HTML => TaintedHtml::class, + TaintKind::INPUT_INCLUDE => TaintedInclude::class, + TaintKind::INPUT_LDAP => TaintedLdap::class, + TaintKind::INPUT_SHELL => TaintedShell::class, + TaintKind::INPUT_SQL => TaintedSql::class, + TaintKind::INPUT_SSRF => TaintedSSRF::class, + TaintKind::INPUT_UNSERIALIZE => TaintedUnserialize::class, + TaintKind::SYSTEM_SECRET => TaintedSystemSecret::class, + TaintKind::USER_SECRET => TaintedUserSecret::class, + ]; + /** + * @var array> + */ + private array $taint_groups = [ + TaintKindGroup::GROUP_INPUT => [ + TaintKind::INPUT_CALLABLE, + TaintKind::INPUT_COOKIE, + TaintKind::INPUT_EVAL, + TaintKind::INPUT_FILE, + TaintKind::INPUT_HAS_QUOTES, + TaintKind::INPUT_HEADER, + TaintKind::INPUT_HTML, + TaintKind::INPUT_INCLUDE, + TaintKind::INPUT_LDAP, + TaintKind::INPUT_SHELL, + TaintKind::INPUT_SQL, + TaintKind::INPUT_SSRF, + TaintKind::INPUT_UNSERIALIZE, + ], + ]; + + /** + * @var class-string + */ + private string $default_type = TaintedCustom::class; + + /** + * @param class-string $class_name + */ + public function addType(string $type, string $class_name, string $group = ''): void + { + if ($type === '') { + throw new LogicException('Taint type cannot be an empty string'); + } + if ($this->hasType($type)) { + throw new RuntimeException('Taint type ' . $type . ' is already defined'); + } + $this->assertClassName($class_name); + $this->taint_types[$type] = $class_name; + if ($group !== '') { + $this->addTypeToGroup($type, $group); + } + } + + /** + * @param non-empty-string $type + */ + public function hasType(string $type): bool + { + return isset($this->taint_types[$type]); + } + + /** + * @param non-empty-string $type + * @return class-string|null + */ + public function getType(string $type): ?string + { + return $this->taint_types[$type] ?? null; + } + + /** + * @return class-string + */ + public function getDefaultType(): string + { + return $this->default_type; + } + + /** + * @psalm-suppress PossiblyUnusedMethod + * @param class-string $class_name + */ + public function setDefaultType(string $class_name): void + { + $this->assertClassName($class_name); + $this->default_type = $class_name; + } + + /** + * @psalm-suppress PossiblyUnusedMethod + * @param non-empty-string $group + */ + public function hasGroup(string $group): bool + { + return isset($this->taint_groups[$group]); + } + + /** + * @param non-empty-string $group + * @return list + */ + public function resolveGroup(string $group): array + { + return $this->taint_groups[$group] ?? []; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $group + * @todo probably make this public + */ + private function addTypeToGroup(string $type, string $group): void + { + if (!$this->hasType($type)) { + throw new RuntimeException('Taint type ' . $type . ' is not defined'); + } + if (!isset($this->taint_groups[$group])) { + $this->taint_groups[$group] = []; + } + $this->taint_groups[$group][] = $type; + } + + private function assertClassName(string $class_name): void + { + if (!class_exists($class_name)) { + throw new LogicException('Taint class ' . $class_name . ' does not exist'); + } + if (!is_subclass_of($class_name, TaintedInput::class)) { + throw new LogicException('Taint class ' . $class_name . ' must be a subclass of ' . TaintedInput::class); + } + if (stripos((new ReflectionClass($class_name))->getShortName(), 'Tainted') !== 0) { + throw new LogicException('Taint class name ' . $class_name . ' must start with "Tainted"'); + } + } +} diff --git a/src/Psalm/Issue/TaintedCallable.php b/src/Psalm/Issue/TaintedCallable.php index 4b988029704..ab264215255 100644 --- a/src/Psalm/Issue/TaintedCallable.php +++ b/src/Psalm/Issue/TaintedCallable.php @@ -5,4 +5,5 @@ final class TaintedCallable extends TaintedInput { public const SHORTCODE = 243; + public const MESSAGE = 'Detected tainted text'; } diff --git a/src/Psalm/Issue/TaintedCookie.php b/src/Psalm/Issue/TaintedCookie.php index 424b860c949..a5ddaaa214e 100644 --- a/src/Psalm/Issue/TaintedCookie.php +++ b/src/Psalm/Issue/TaintedCookie.php @@ -5,4 +5,5 @@ final class TaintedCookie extends TaintedInput { public const SHORTCODE = 257; + public const MESSAGE = 'Detected tainted cookie'; } diff --git a/src/Psalm/Issue/TaintedCustom.php b/src/Psalm/Issue/TaintedCustom.php index 2bd91c65caf..097ffe6d1f4 100644 --- a/src/Psalm/Issue/TaintedCustom.php +++ b/src/Psalm/Issue/TaintedCustom.php @@ -5,4 +5,5 @@ final class TaintedCustom extends TaintedInput { public const SHORTCODE = 249; + public const MESSAGE = 'Detected tainted %s'; } diff --git a/src/Psalm/Issue/TaintedEval.php b/src/Psalm/Issue/TaintedEval.php index b003c5373f4..ab5fe87020c 100644 --- a/src/Psalm/Issue/TaintedEval.php +++ b/src/Psalm/Issue/TaintedEval.php @@ -5,4 +5,5 @@ final class TaintedEval extends TaintedInput { public const SHORTCODE = 252; + public const MESSAGE = 'Detected tainted code passed to eval or similar'; } diff --git a/src/Psalm/Issue/TaintedFile.php b/src/Psalm/Issue/TaintedFile.php index 1c1838574b8..287337832a8 100644 --- a/src/Psalm/Issue/TaintedFile.php +++ b/src/Psalm/Issue/TaintedFile.php @@ -5,4 +5,5 @@ final class TaintedFile extends TaintedInput { public const SHORTCODE = 255; + public const MESSAGE = 'Detected tainted file handling'; } diff --git a/src/Psalm/Issue/TaintedHeader.php b/src/Psalm/Issue/TaintedHeader.php index ebbbfc5f239..7ac72984cbe 100644 --- a/src/Psalm/Issue/TaintedHeader.php +++ b/src/Psalm/Issue/TaintedHeader.php @@ -5,4 +5,5 @@ final class TaintedHeader extends TaintedInput { public const SHORTCODE = 256; + public const MESSAGE = 'Detected tainted header'; } diff --git a/src/Psalm/Issue/TaintedHtml.php b/src/Psalm/Issue/TaintedHtml.php index 3c9956da866..593e6069c32 100644 --- a/src/Psalm/Issue/TaintedHtml.php +++ b/src/Psalm/Issue/TaintedHtml.php @@ -5,4 +5,5 @@ final class TaintedHtml extends TaintedInput { public const SHORTCODE = 245; + public const MESSAGE = 'Detected tainted HTML'; } diff --git a/src/Psalm/Issue/TaintedInclude.php b/src/Psalm/Issue/TaintedInclude.php index f1affa5c6c5..6680cd6b52c 100644 --- a/src/Psalm/Issue/TaintedInclude.php +++ b/src/Psalm/Issue/TaintedInclude.php @@ -5,4 +5,5 @@ final class TaintedInclude extends TaintedInput { public const SHORTCODE = 251; + public const MESSAGE = 'Detected tainted code passed to include or similar'; } diff --git a/src/Psalm/Issue/TaintedInput.php b/src/Psalm/Issue/TaintedInput.php index 15713c09335..2d334f42585 100644 --- a/src/Psalm/Issue/TaintedInput.php +++ b/src/Psalm/Issue/TaintedInput.php @@ -10,6 +10,8 @@ abstract class TaintedInput extends CodeIssue public const ERROR_LEVEL = -2; /** @var int<0, max> */ public const SHORTCODE = 205; + /** @var string */ + public const MESSAGE = 'Detected generic tainted input'; /** * @var string diff --git a/src/Psalm/Issue/TaintedLdap.php b/src/Psalm/Issue/TaintedLdap.php index 4ab8e32e2ff..762618bc387 100644 --- a/src/Psalm/Issue/TaintedLdap.php +++ b/src/Psalm/Issue/TaintedLdap.php @@ -5,4 +5,5 @@ final class TaintedLdap extends TaintedInput { public const SHORTCODE = 254; + public const MESSAGE = 'Detected tainted LDAP request'; } diff --git a/src/Psalm/Issue/TaintedSSRF.php b/src/Psalm/Issue/TaintedSSRF.php index def8a2cdc97..236812f1868 100644 --- a/src/Psalm/Issue/TaintedSSRF.php +++ b/src/Psalm/Issue/TaintedSSRF.php @@ -5,4 +5,5 @@ final class TaintedSSRF extends TaintedInput { public const SHORTCODE = 253; + public const MESSAGE = 'Detected tainted network request'; } diff --git a/src/Psalm/Issue/TaintedShell.php b/src/Psalm/Issue/TaintedShell.php index 8ff52b1615f..bd6b4a39285 100644 --- a/src/Psalm/Issue/TaintedShell.php +++ b/src/Psalm/Issue/TaintedShell.php @@ -5,4 +5,5 @@ final class TaintedShell extends TaintedInput { public const SHORTCODE = 246; + public const MESSAGE = 'Detected tainted shell code'; } diff --git a/src/Psalm/Issue/TaintedSql.php b/src/Psalm/Issue/TaintedSql.php index 9d790d71e62..e3525b31996 100644 --- a/src/Psalm/Issue/TaintedSql.php +++ b/src/Psalm/Issue/TaintedSql.php @@ -5,4 +5,5 @@ final class TaintedSql extends TaintedInput { public const SHORTCODE = 244; + public const MESSAGE = 'Detected tainted SQL'; } diff --git a/src/Psalm/Issue/TaintedSystemSecret.php b/src/Psalm/Issue/TaintedSystemSecret.php index 04888a99a37..b51a882e425 100644 --- a/src/Psalm/Issue/TaintedSystemSecret.php +++ b/src/Psalm/Issue/TaintedSystemSecret.php @@ -5,4 +5,5 @@ final class TaintedSystemSecret extends TaintedInput { public const SHORTCODE = 248; + public const MESSAGE = 'Detected tainted system secret leaking'; } diff --git a/src/Psalm/Issue/TaintedTextWithQuotes.php b/src/Psalm/Issue/TaintedTextWithQuotes.php index 9b78c3d4509..21e31e50311 100644 --- a/src/Psalm/Issue/TaintedTextWithQuotes.php +++ b/src/Psalm/Issue/TaintedTextWithQuotes.php @@ -5,4 +5,5 @@ final class TaintedTextWithQuotes extends TaintedInput { public const SHORTCODE = 274; + public const MESSAGE = 'Detected tainted text with possible quotes'; } diff --git a/src/Psalm/Issue/TaintedUnserialize.php b/src/Psalm/Issue/TaintedUnserialize.php index b2e97c9a438..31143f7c0d2 100644 --- a/src/Psalm/Issue/TaintedUnserialize.php +++ b/src/Psalm/Issue/TaintedUnserialize.php @@ -5,4 +5,5 @@ final class TaintedUnserialize extends TaintedInput { public const SHORTCODE = 250; + public const MESSAGE = 'Detected tainted code passed to unserialize or similar'; } diff --git a/src/Psalm/Issue/TaintedUserSecret.php b/src/Psalm/Issue/TaintedUserSecret.php index 5bbfcf17142..27b84abbf54 100644 --- a/src/Psalm/Issue/TaintedUserSecret.php +++ b/src/Psalm/Issue/TaintedUserSecret.php @@ -5,4 +5,5 @@ final class TaintedUserSecret extends TaintedInput { public const SHORTCODE = 247; + public const MESSAGE = 'Detected tainted user secret leaking'; } diff --git a/src/Psalm/Type/TaintKindGroup.php b/src/Psalm/Type/TaintKindGroup.php index eb74a2916a6..e0f3e2f0e2a 100644 --- a/src/Psalm/Type/TaintKindGroup.php +++ b/src/Psalm/Type/TaintKindGroup.php @@ -7,6 +7,11 @@ */ final class TaintKindGroup { + public const GROUP_INPUT = 'input'; + + /** + * @deprecated since Psalm 5.x, use `TaintTypeRegistry::resolveGroup(TaintKindGroup::GROUP_INPUT)` instead + */ public const ALL_INPUT = [ TaintKind::INPUT_HTML, TaintKind::INPUT_HAS_QUOTES, diff --git a/tests/TaintTest.php b/tests/TaintTest.php index 6439b366c1d..10f6fb67734 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -6,6 +6,7 @@ use Psalm\Exception\CodeException; use Psalm\Internal\Analyzer\IssueData; use Psalm\IssueBuffer; +use Psalm\Tests\fixtures\Issue\TaintedTestingAnything; use function array_map; use function preg_quote; @@ -2609,4 +2610,53 @@ function triggerFile(string $value): string ], ]; } + + /** + * @test + */ + public function customTaintedInputImplementationIsDetected(): void + { + // disables issue exceptions - we need all, not just the first + $this->testConfig->throw_exception = false; + $this->project_analyzer->trackTaintedInputs(); + // registers test fixture `\Psalm\Tests\fixtures\Issue\TaintedTestingAnything` + $this->testConfig->taint_type_registry->addType('testing-anything', TaintedTestingAnything::class); + + $code = 'addFile($filePath, $code); + $this->analyzeFile($filePath, new Context(), false); + + $actualIssueTypes = array_map( + static fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', + IssueBuffer::getIssuesDataForFile($filePath), + ); + $expectedIssuesTypes = [ + 'TaintedTestingAnything{ function sinkTestingAnything($value): void {} }', + // @todo `TaintKindGroup` needs to be extensible + // 'TaintedTestingAnything{ function sinkInput($value): void {} }', + ]; + + self::assertSame($expectedIssuesTypes, $actualIssueTypes); + } } diff --git a/tests/fixtures/Issue/TaintedTestingAnything.php b/tests/fixtures/Issue/TaintedTestingAnything.php new file mode 100644 index 00000000000..0bc60cde5f4 --- /dev/null +++ b/tests/fixtures/Issue/TaintedTestingAnything.php @@ -0,0 +1,15 @@ +