diff --git a/docs/security_analysis/custom_taint_sources.md b/docs/security_analysis/custom_taint_sources.md
index d3bdb0a3205..938bd3455ba 100644
--- a/docs/security_analysis/custom_taint_sources.md
+++ b/docs/security_analysis/custom_taint_sources.md
@@ -23,52 +23,32 @@ For example this plugin treats all variables named `$bad_data` as taint sources.
```php
*/
- public static function afterExpressionAnalysis(AfterExpressionAnalysisEvent $event): ?bool {
+ public static function addTaints(AddRemoveTaintsEvent $event): array
+ {
$expr = $event->getExpr();
- $statements_source = $event->getStatementsSource();
- $codebase = $event->getCodebase();
- if ($expr instanceof PhpParser\Node\Expr\Variable
- && $expr->name === 'bad_data'
- ) {
- $expr_type = $statements_source->getNodeTypeProvider()->getType($expr);
- // should be a globally unique id
- // you can use its line number/start offset
- $expr_identifier = '$bad_data'
- . '-' . $statements_source->getFileName()
- . ':' . $expr->getAttribute('startFilePos');
-
- if ($expr_type) {
- $codebase->addTaintSource(
- $expr_type,
- $expr_identifier,
- TaintKindGroup::ALL_INPUT,
- new CodeLocation($statements_source, $expr)
- );
- }
+ if ($expr instanceof Variable && $expr->name === 'bad_data') {
+ return TaintKindGroup::ALL_INPUT;
}
- return null;
+
+ return [];
}
}
```
diff --git a/examples/plugins/TaintActiveRecords.php b/examples/plugins/TaintActiveRecords.php
new file mode 100644
index 00000000000..3214e56525a
--- /dev/null
+++ b/examples/plugins/TaintActiveRecords.php
@@ -0,0 +1,88 @@
+
+ */
+ public static function addTaints(AddRemoveTaintsEvent $event): array
+ {
+ $expr = $event->getExpr();
+ $statements_source = $event->getStatementsSource();
+
+ // For all property fetch expressions, walk through the full fetch path
+ // (e.g. `$model->property->subproperty`) and check if it contains
+ // any class of namespace \app\models\
+ do {
+ $expr_type = $statements_source->getNodeTypeProvider()->getType($expr);
+ if (!$expr_type) {
+ continue;
+ }
+
+ if (self::containsActiveRecord($expr_type)) {
+ return TaintKindGroup::ALL_INPUT;
+ }
+ } while ($expr = self::getParentNode($expr));
+
+ return [];
+ }
+
+ /**
+ * @return bool `true` if union contains a type of model
+ */
+ private static function containsActiveRecord(Union $union_type): bool
+ {
+ foreach ($union_type->getAtomicTypes() as $type) {
+ if (self::isActiveRecord($type)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return bool `true` if namespace of type is in namespace `app\models`
+ */
+ private static function isActiveRecord(Atomic $type): bool
+ {
+ if (!$type instanceof TNamedObject) {
+ return false;
+ }
+
+
+ return strpos($type->value, 'app\models\\') === 0;
+ }
+
+
+ /**
+ * Return next node that should be followed for active record search
+ */
+ private static function getParentNode(Expr $expr): ?Expr
+ {
+ // Model properties are always accessed by a property fetch
+ if ($expr instanceof PropertyFetch) {
+ return $expr->var;
+ }
+
+ return null;
+ }
+}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
index b6380df0fcd..01e60cdadc0 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
@@ -12,6 +12,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TypeCombiner;
use Psalm\Issue\DuplicateArrayKey;
@@ -442,6 +443,11 @@ private static function analyzeArrayItem(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($new_parent_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
foreach ($item_value_type->parent_nodes as $parent_node) {
$data_flow_graph->addPath(
$parent_node,
@@ -477,6 +483,11 @@ private static function analyzeArrayItem(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($new_parent_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
foreach ($item_key_type->parent_nodes as $parent_node) {
$data_flow_graph->addPath(
$parent_node,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php
index bedec945c25..d67616540a5 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php
@@ -26,6 +26,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
@@ -503,6 +504,11 @@ private static function taintProperty(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($property_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
$data_flow_graph->addPath(
$property_node,
$var_node,
@@ -598,6 +604,11 @@ public static function taintUnspecializedProperty(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($property_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
$data_flow_graph->addPath(
$localized_property_node,
$property_node,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
index 679abaa347a..a55d595e8c2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
@@ -31,6 +31,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\ReferenceConstraint;
use Psalm\Internal\Scanner\VarDocblockComment;
@@ -821,6 +822,11 @@ private static function taintAssignment(
$data_flow_graph->addNode($new_parent_node);
$new_parent_nodes = [$new_parent_node->id => $new_parent_node];
+ if ($added_taints !== [] && $data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($new_parent_node);
+ $data_flow_graph->addSource($taint_source);
+ }
+
foreach ($parent_nodes as $parent_node) {
$data_flow_graph->addPath(
$parent_node,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
index 2b7b8b607ae..248a160097b 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
@@ -16,6 +16,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\MethodIdentifier;
use Psalm\Issue\DocblockTypeContradiction;
use Psalm\Issue\ImpureMethodCall;
@@ -169,6 +170,11 @@ public static function analyze(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($new_parent_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
if ($stmt_left_type && $stmt_left_type->parent_nodes) {
foreach ($stmt_left_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
index 2fd7cd1b5a7..1c1bf669414 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
@@ -20,6 +20,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
@@ -1897,5 +1898,10 @@ private static function processTaintedness(
$removed_taints,
);
}
+
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($argument_value_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php
index 98e192c72f7..09ac0a40865 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php
@@ -17,6 +17,7 @@
use Psalm\Internal\Codebase\InternalCallMapHandler;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\TaintSink;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
use Psalm\Internal\Type\TemplateResult;
@@ -866,6 +867,11 @@ private static function getAnalyzeNamedExpression(
$removed_taints,
);
}
+
+ if ($added_taints !== []) {
+ $taint_source = TaintSource::fromNode($custom_call_sink);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php
index 8f35fee56b3..ae3ecb72155 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php
@@ -19,6 +19,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\DataFlow\TaintSink;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
@@ -738,6 +739,11 @@ private static function analyzeConstructorExpression(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== []) {
+ $taint_source = TaintSource::fromNode($custom_call_sink);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
foreach ($stmt_class_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
$parent_node,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php
index 63c815ff521..c5b774238f4 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php
@@ -8,7 +8,9 @@
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
+use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralFloat;
@@ -101,6 +103,11 @@ public static function analyze(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($new_parent_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
if ($casted_part_type->parent_nodes) {
foreach ($casted_part_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php
index 3f6b2a8d19b..1f8cb15e4f9 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php
@@ -9,6 +9,7 @@
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\TaintSink;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Issue\ForbiddenCode;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
@@ -60,6 +61,11 @@ public static function analyze(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== []) {
+ $taint_source = TaintSource::fromNode($eval_param_sink);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
foreach ($expr_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
$parent_node,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
index 9bc860de44d..b04cea8cc0d 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
@@ -16,6 +16,7 @@
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\Type\Comparator\AtomicTypeComparator;
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
@@ -460,6 +461,11 @@ public static function taintArrayFetch(
$stmt_type = $stmt_type->setParentNodes([$new_parent_node->id => $new_parent_node]);
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($new_parent_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
if ($array_key_node) {
$offset_type = $offset_type->setParentNodes([$array_key_node->id => $array_key_node]);
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php
index 6ad6983b76e..962ff335c80 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php
@@ -22,6 +22,7 @@
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
@@ -899,6 +900,11 @@ public static function processTaints(
}
$type = $type->setParentNodes([$property_node->id => $property_node], true);
+
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($var_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
}
} else {
self::processUnspecialTaints(
@@ -975,6 +981,11 @@ public static function processUnspecialTaints(
}
$type = $type->setParentNodes([$localized_property_node->id => $localized_property_node], true);
+
+ if ($added_taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
+ $taint_source = TaintSource::fromNode($localized_property_node);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
}
private static function handleEnumName(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php
index 6c4a36ab2bf..55f14ae7132 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php
@@ -14,6 +14,7 @@
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\DataFlow\TaintSink;
+use Psalm\Internal\DataFlow\TaintSource;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Issue\MissingFile;
use Psalm\Issue\UnresolvableInclude;
@@ -132,6 +133,11 @@ public static function analyze(
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
+ if ($added_taints !== []) {
+ $taint_source = TaintSource::fromNode($include_param_sink);
+ $statements_analyzer->data_flow_graph->addSource($taint_source);
+ }
+
foreach ($stmt_expr_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
$parent_node,
diff --git a/src/Psalm/Internal/DataFlow/TaintSource.php b/src/Psalm/Internal/DataFlow/TaintSource.php
index 1777afe99f7..a69cf2eb1ce 100644
--- a/src/Psalm/Internal/DataFlow/TaintSource.php
+++ b/src/Psalm/Internal/DataFlow/TaintSource.php
@@ -7,4 +7,14 @@
*/
final class TaintSource extends DataFlowNode
{
+ public static function fromNode(DataFlowNode $node): self
+ {
+ return new self(
+ $node->id,
+ $node->label,
+ $node->code_location,
+ $node->specialization_key,
+ $node->taints,
+ );
+ }
}
diff --git a/tests/Config/Plugin/EventHandler/AddTaints/AddTaintsInterfaceTest.php b/tests/Config/Plugin/EventHandler/AddTaints/AddTaintsInterfaceTest.php
new file mode 100644
index 00000000000..2fd9f389282
--- /dev/null
+++ b/tests/Config/Plugin/EventHandler/AddTaints/AddTaintsInterfaceTest.php
@@ -0,0 +1,286 @@
+setIncludeCollector(new IncludeCollector());
+ return new ProjectAnalyzer(
+ $config,
+ new Providers(
+ $this->file_provider,
+ new FakeParserCacheProvider(),
+ ),
+ new ReportOptions(),
+ );
+ }
+
+ public function setUp(): void
+ {
+ RuntimeCaches::clearAll();
+ $this->file_provider = new FakeFileProvider();
+ }
+
+ public function testTaintBadDataVariables(): void
+ {
+ $this->project_analyzer = $this->getProjectAnalyzerWithConfig(
+ TestConfig::loadFromXML(
+ dirname(__DIR__, 5) . DIRECTORY_SEPARATOR,
+ '
+