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 21 commits into
base: 5.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f01ad7b
fix: add taint source on plugin-added taints
Patrick-Remy Sep 15, 2023
e24f274
style: fix code style and redundant conditions
Patrick-Remy Sep 15, 2023
fa2736b
test: add addTaints plugin test
Patrick-Remy Sep 19, 2023
d85ce42
docs: update custom taint plugin docs
Patrick-Remy Sep 19, 2023
a0ea8e4
refactor: only add taint sources if required
Patrick-Remy Sep 19, 2023
dded6ea
test: move event handler tests to subfolder
Patrick-Remy Jan 23, 2024
05cf8f0
docs: fix example for custom taint sources
Patrick-Remy Jan 23, 2024
6ecaae6
test: simplify taint tests for variable assignment
Patrick-Remy Jan 23, 2024
8f0da9e
docs: simplify TaintBadDataPlugin example again
Patrick-Remy Jan 23, 2024
ba334db
fix: add missing dispatch of taints event for assignment value
Patrick-Remy Dec 31, 2024
4dbc74f
fix: generate taint sources for function calls/returns
Patrick-Remy Jan 19, 2025
5471bb1
fix: generate taint sources for methods
Patrick-Remy Jan 19, 2025
4af11dc
test: refactor AddTaintsInterfaceTest
Patrick-Remy Jan 19, 2025
55dcb03
test: add taint test for static method and proxy calls
Patrick-Remy Jan 19, 2025
79c4a7e
style: fix code style to match project requirements
Patrick-Remy Jan 19, 2025
f095412
fix: revert adding expr-identifier for func-calls
Patrick-Remy Jan 19, 2025
ca06f34
style: fix psalm e2e test findings
Patrick-Remy Jan 19, 2025
7915e1e
test: fix added taint-flow path in var-assignment
Patrick-Remy Jan 19, 2025
b5875c1
refactor: extract code from assignment analyzer into methods
Patrick-Remy Jan 19, 2025
65b306f
refactor: remove change of assignment data flow graph
Patrick-Remy Jan 19, 2025
3ea4dd3
fix: taint variable fetches in every case
Patrick-Remy Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
21 changes: 20 additions & 1 deletion src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Psalm\FileManipulation;
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeCollector;
use Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallReturnTypeFetcher;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
Expand Down Expand Up @@ -833,6 +834,24 @@ public function analyze(
}
}

// Class methods are analyzed deferred, therefor it's required to
// add taint sources additionally on analyze not only on call
if ($codebase->taint_flow_graph
&& $this->function instanceof ClassMethod
&& $cased_method_id) {
$method_source = DataFlowNode::getForMethodReturn(
(string) $method_id,
$cased_method_id,
$storage->location,
);

FunctionCallReturnTypeFetcher::taintUsingStorage(
$storage,
$codebase->taint_flow_graph,
$method_source,
);
}

if ($add_mutations) {
if ($this->return_vars_in_scope !== null) {
$context->vars_in_scope = TypeAnalyzer::combineKeyedTypes(
Expand Down Expand Up @@ -1070,7 +1089,7 @@ private function processParams(

$statements_analyzer->data_flow_graph->addNode($param_assignment);

if ($cased_method_id) {
if ($cased_method_id !== null) {
$type_source = DataFlowNode::getForMethodArgument(
$cased_method_id,
$cased_method_id,
Expand Down
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
Loading
Loading