Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add taint source on plugin-added taints #10206

Open
wants to merge 9 commits into
base: 5.x
Choose a base branch
from
52 changes: 16 additions & 36 deletions docs/security_analysis/custom_taint_sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,32 @@ For example this plugin treats all variables named `$bad_data` as taint sources.
```php
<?php

namespace Some\Ns;
namespace Psalm\Example\Plugin;

use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\FileManipulation;
use Psalm\Plugin\EventHandler\AfterExpressionAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent;
use PhpParser\Node\Expr\Variable;
use Psalm\Plugin\EventHandler\AddTaintsInterface;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type\TaintKindGroup;

class BadSqlTainter implements AfterExpressionAnalysisInterface
/**
* Add input taints to all variables named 'bad_data'
*/
class TaintBadDataPlugin implements AddTaintsInterface
{
/**
* Called after an expression has been checked
*
* @param PhpParser\Node\Expr $expr
* @param Context $context
* @param FileManipulation[] $file_replacements
* Called to see what taints should be added
*
* @return void
* @return list<string>
*/
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 [];
}
}
```
88 changes: 88 additions & 0 deletions examples/plugins/TaintActiveRecords.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Psalm\Example\Plugin;

use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr;
use Psalm\Plugin\EventHandler\AddTaintsInterface;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\TaintKindGroup;
use Psalm\Type\Union;

/**
* Marks all property fetches of models inside namespace \app\models as tainted.
* ActiveRecords are model-representation of database entries, which can always
* contain user-input and therefor should be tainted.
*/
class TaintActiveRecords implements AddTaintsInterface
{
/**
* Called to see what taints should be added
*
* @return list<string>
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);
}
Expand Down
Loading
Loading