1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Improve tainting of specializable classes

This commit is contained in:
Brown 2020-06-19 01:22:51 -04:00
parent 078b8b7b1a
commit 8f2e28c36b
15 changed files with 726 additions and 303 deletions

View File

@ -888,12 +888,17 @@ class CommentAnalyzer
) {
$info->mutation_free = true;
$info->external_mutation_free = true;
$info->taint_specialize = true;
}
if (isset($parsed_docblock->tags['psalm-external-mutation-free'])) {
$info->external_mutation_free = true;
}
if (isset($parsed_docblock->tags['psalm-taint-specialize'])) {
$info->taint_specialize = true;
}
if (isset($parsed_docblock->tags['psalm-override-property-visibility'])) {
$info->override_property_visibility = true;
}

View File

@ -748,6 +748,30 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
}
}
if ($codebase->taint
&& $this->function instanceof ClassMethod
&& $cased_method_id
&& $this->function->name->name === '__construct'
&& isset($context->vars_in_scope['$this'])
&& $context->vars_in_scope['$this']->parent_nodes
) {
$method_source = TaintNode::getForMethodReturn(
(string) $method_id,
$cased_method_id,
$storage->location
);
$codebase->taint->addTaintNode($method_source);
foreach ($context->vars_in_scope['$this']->parent_nodes as $parent_node) {
$codebase->taint->addPath(
$parent_node,
$method_source,
'$this'
);
}
}
if ($add_mutations) {
if ($this->return_vars_in_scope !== null) {
$context->vars_in_scope = TypeAnalyzer::combineKeyedTypes(

View File

@ -690,7 +690,7 @@ class ArrayAssignmentAnalyzer
if ($root_array_expr instanceof PhpParser\Node\Expr\PropertyFetch) {
if ($root_array_expr->name instanceof PhpParser\Node\Identifier) {
PropertyAssignmentAnalyzer::analyzeInstance(
InstancePropertyAssignmentAnalyzer::analyze(
$statements_analyzer,
$root_array_expr,
$root_array_expr->name->name,
@ -711,7 +711,7 @@ class ArrayAssignmentAnalyzer
} elseif ($root_array_expr instanceof PhpParser\Node\Expr\StaticPropertyFetch
&& $root_array_expr->name instanceof PhpParser\Node\Identifier
) {
PropertyAssignmentAnalyzer::analyzeStatic(
StaticPropertyAssignmentAnalyzer::analyze(
$statements_analyzer,
$root_array_expr,
null,

View File

@ -50,7 +50,7 @@ use Psalm\Internal\Taint\TaintNode;
/**
* @internal
*/
class PropertyAssignmentAnalyzer
class InstancePropertyAssignmentAnalyzer
{
/**
* @param StatementsAnalyzer $statements_analyzer
@ -63,7 +63,7 @@ class PropertyAssignmentAnalyzer
*
* @return false|null
*/
public static function analyzeInstance(
public static function analyze(
StatementsAnalyzer $statements_analyzer,
$stmt,
$prop_name,
@ -402,7 +402,17 @@ class PropertyAssignmentAnalyzer
$has_regular_setter = true;
$property_exists = true;
self::taintProperty($statements_analyzer, $stmt, $property_id, $assignment_value_type);
if (!$context->collect_initializations) {
self::taintProperty(
$statements_analyzer,
$stmt,
$property_id,
$class_storage,
$assignment_value_type,
$context
);
}
continue;
}
}
@ -463,7 +473,16 @@ class PropertyAssignmentAnalyzer
* not in that list, fall through
*/
if (!$var_id || !$class_storage->sealed_properties) {
self::taintProperty($statements_analyzer, $stmt, $property_id, $assignment_value_type);
if (!$context->collect_initializations) {
self::taintProperty(
$statements_analyzer,
$stmt,
$property_id,
$class_storage,
$assignment_value_type,
$context
);
}
continue;
}
@ -506,7 +525,18 @@ class PropertyAssignmentAnalyzer
}
}
self::taintProperty($statements_analyzer, $stmt, $property_id, $assignment_value_type);
if ($codebase->taint && !$context->collect_initializations) {
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
self::taintProperty(
$statements_analyzer,
$stmt,
$property_id,
$class_storage,
$assignment_value_type,
$context
);
}
if (!$codebase->properties->propertyExists(
$property_id,
@ -1051,7 +1081,7 @@ class PropertyAssignmentAnalyzer
ExpressionAnalyzer::analyze($statements_analyzer, $prop->default, $context);
if ($prop_default_type = $statements_analyzer->node_data->getType($prop->default)) {
if (self::analyzeInstance(
if (self::analyze(
$statements_analyzer,
$prop,
$prop->name->name,
@ -1070,7 +1100,9 @@ class PropertyAssignmentAnalyzer
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\PropertyFetch $stmt,
string $property_id,
Type\Union $assignment_value_type
\Psalm\Storage\ClassLikeStorage $class_storage,
Type\Union $assignment_value_type,
Context $context
) : void {
$codebase = $statements_analyzer->getCodebase();
@ -1078,309 +1110,90 @@ class PropertyAssignmentAnalyzer
return;
}
$code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
$var_location = new CodeLocation($statements_analyzer->getSource(), $stmt->var);
$property_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
$localized_property_node = new TaintNode(
$property_id . '-' . $code_location->file_name . ':' . $code_location->raw_file_start,
$property_id,
$code_location,
null
);
$codebase->taint->addTaintNode($localized_property_node);
$property_node = new TaintNode(
$property_id,
$property_id,
null,
null
);
$codebase->taint->addTaintNode($property_node);
$codebase->taint->addPath($localized_property_node, $property_node, 'property-assignment');
if ($assignment_value_type->parent_nodes) {
foreach ($assignment_value_type->parent_nodes as $parent_node) {
$codebase->taint->addPath($parent_node, $localized_property_node, '=');
}
}
}
/**
* @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr\StaticPropertyFetch $stmt
* @param PhpParser\Node\Expr|null $assignment_value
* @param Type\Union $assignment_value_type
* @param Context $context
*
* @return false|null
*/
public static function analyzeStatic(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\StaticPropertyFetch $stmt,
$assignment_value,
Type\Union $assignment_value_type,
Context $context
) {
$var_id = ExpressionIdentifier::getArrayVarId(
$stmt,
$context->self,
$statements_analyzer
);
$fq_class_name = (string) $statements_analyzer->node_data->getType($stmt->class);
$codebase = $statements_analyzer->getCodebase();
$prop_name = $stmt->name;
if (!$prop_name instanceof PhpParser\Node\Identifier) {
if (ExpressionAnalyzer::analyze($statements_analyzer, $prop_name, $context) === false) {
return false;
}
if ($fq_class_name && !$context->ignore_variable_property) {
$codebase->analyzer->addMixedMemberName(
strtolower($fq_class_name) . '::$',
$context->calling_method_id ?: $statements_analyzer->getFileName()
);
}
return;
}
$property_id = $fq_class_name . '::$' . $prop_name;
if (!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)) {
if (IssueBuffer::accepts(
new UndefinedPropertyAssignment(
'Static property ' . $property_id . ' is not defined',
new CodeLocation($statements_analyzer->getSource(), $stmt),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
$declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
$fq_class_name . '::$' . $prop_name->name,
false
);
$declaring_property_id = strtolower((string) $declaring_property_class) . '::$' . $prop_name;
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
if ($class_storage->specialize_instance) {
$var_id = ExpressionIdentifier::getArrayVarId(
$stmt->var,
null,
$statements_analyzer
);
if (!$moved_class) {
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern) {
list($old_declaring_fq_class_name) = explode('::$', $declaring_property_id);
list($new_fq_class_name, $new_property_name) = explode('::$', $transformation);
$var_property_id = ExpressionIdentifier::getArrayVarId(
$stmt,
null,
$statements_analyzer
);
$file_manipulations = [];
if ($var_id) {
$var_node = TaintNode::getForAssignment(
$var_id,
$var_location
);
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
}
$codebase->taint->addTaintNode($var_node);
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
$property_node = TaintNode::getForAssignment(
$var_property_id ?: $var_id . '->$property',
$property_location
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
$codebase->taint->addTaintNode($property_node);
$codebase->taint->addPath(
$property_node,
$var_node,
'property-assignment'
. ($stmt->name instanceof PhpParser\Node\Identifier ? '-' . $stmt->name : '')
);
if ($assignment_value_type->parent_nodes) {
foreach ($assignment_value_type->parent_nodes as $parent_node) {
$codebase->taint->addPath($parent_node, $property_node, '=');
}
}
}
}
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
$stmt_var_type = clone $context->vars_in_scope[$var_id];
$property_storage = $class_storage->properties[$prop_name->name];
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
$class_property_type = $codebase->properties->getPropertyType(
$property_id,
true,
$statements_analyzer,
$context
);
if (!$class_property_type) {
$class_property_type = Type::getMixed();
if (!$assignment_value_type->hasMixed()) {
if ($property_storage->suggested_type) {
$property_storage->suggested_type = Type::combineUnionTypes(
$assignment_value_type,
$property_storage->suggested_type
);
} else {
$property_storage->suggested_type = Type::combineUnionTypes(
Type::getNull(),
$assignment_value_type
);
if ($context->vars_in_scope[$var_id]->parent_nodes) {
foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) {
$codebase->taint->addPath($parent_node, $var_node, '=');
}
}
$stmt_var_type->parent_nodes = [$var_node];
$context->vars_in_scope[$var_id] = $stmt_var_type;
}
} else {
$class_property_type = clone $class_property_type;
}
$code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
if ($assignment_value_type->hasMixed()) {
return null;
}
$localized_property_node = new TaintNode(
$property_id . '-' . $code_location->file_name . ':' . $code_location->raw_file_start,
$property_id,
$code_location,
null
);
if ($class_property_type->hasMixed()) {
return null;
}
$codebase->taint->addTaintNode($localized_property_node);
$class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$class_property_type,
$fq_class_name,
$fq_class_name,
$class_storage->parent_class
);
$property_node = new TaintNode(
$property_id,
$property_id,
null,
null
);
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
$codebase->taint->addTaintNode($property_node);
$type_match_found = TypeAnalyzer::isContainedBy(
$codebase,
$assignment_value_type,
$class_property_type,
true,
true,
$union_comparison_results
);
$codebase->taint->addPath($localized_property_node, $property_node, 'property-assignment');
if ($union_comparison_results->type_coerced) {
if ($union_comparison_results->type_coerced_from_mixed) {
if (IssueBuffer::accepts(
new MixedPropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type `' . $assignment_value_type->getId() . '` provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
} else {
if (IssueBuffer::accepts(
new PropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type \'' . $assignment_value_type->getId() . '\' provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
if ($assignment_value_type->parent_nodes) {
foreach ($assignment_value_type->parent_nodes as $parent_node) {
$codebase->taint->addPath($parent_node, $localized_property_node, '=');
}
}
}
if ($union_comparison_results->to_string_cast) {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
$var_id . ' expects \'' . $class_property_type . '\', '
. '\'' . $assignment_value_type . '\' provided with a __toString method',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (!$type_match_found && !$union_comparison_results->type_coerced) {
if (TypeAnalyzer::canBeContainedBy($codebase, $assignment_value_type, $class_property_type)) {
if (IssueBuffer::accepts(
new PossiblyInvalidPropertyAssignmentValue(
$var_id . ' with declared type \''
. $class_property_type->getId() . '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new InvalidPropertyAssignmentValue(
$var_id . ' with declared type \'' . $class_property_type->getId()
. '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
}
}
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
return null;
}
}

View File

@ -0,0 +1,331 @@
<?php
namespace Psalm\Internal\Analyzer\Statements\Expression\Assignment;
use PhpParser;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Stmt\PropertyProperty;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\InstancePropertyFetchAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\DeprecatedProperty;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\InaccessibleProperty;
use Psalm\Issue\InternalProperty;
use Psalm\Issue\InvalidPropertyAssignment;
use Psalm\Issue\InvalidPropertyAssignmentValue;
use Psalm\Issue\LoopInvalidation;
use Psalm\Issue\MixedAssignment;
use Psalm\Issue\MixedPropertyAssignment;
use Psalm\Issue\MixedPropertyTypeCoercion;
use Psalm\Issue\NoInterfaceProperties;
use Psalm\Issue\NullPropertyAssignment;
use Psalm\Issue\PossiblyFalsePropertyAssignmentValue;
use Psalm\Issue\PossiblyInvalidPropertyAssignment;
use Psalm\Issue\PossiblyInvalidPropertyAssignmentValue;
use Psalm\Issue\PossiblyNullPropertyAssignment;
use Psalm\Issue\PossiblyNullPropertyAssignmentValue;
use Psalm\Issue\PropertyTypeCoercion;
use Psalm\Issue\UndefinedClass;
use Psalm\Issue\UndefinedPropertyAssignment;
use Psalm\Issue\UndefinedMagicPropertyAssignment;
use Psalm\Issue\UndefinedThisPropertyAssignment;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObject;
use function count;
use function in_array;
use function strtolower;
use function explode;
use Psalm\Internal\Taint\TaintNode;
/**
* @internal
*/
class StaticPropertyAssignmentAnalyzer
{
/**
* @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr\StaticPropertyFetch $stmt
* @param PhpParser\Node\Expr|null $assignment_value
* @param Type\Union $assignment_value_type
* @param Context $context
*
* @return false|null
*/
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\StaticPropertyFetch $stmt,
$assignment_value,
Type\Union $assignment_value_type,
Context $context
) {
$var_id = ExpressionIdentifier::getArrayVarId(
$stmt,
$context->self,
$statements_analyzer
);
$fq_class_name = (string) $statements_analyzer->node_data->getType($stmt->class);
$codebase = $statements_analyzer->getCodebase();
$prop_name = $stmt->name;
if (!$prop_name instanceof PhpParser\Node\Identifier) {
if (ExpressionAnalyzer::analyze($statements_analyzer, $prop_name, $context) === false) {
return false;
}
if ($fq_class_name && !$context->ignore_variable_property) {
$codebase->analyzer->addMixedMemberName(
strtolower($fq_class_name) . '::$',
$context->calling_method_id ?: $statements_analyzer->getFileName()
);
}
return;
}
$property_id = $fq_class_name . '::$' . $prop_name;
if (!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)) {
if (IssueBuffer::accepts(
new UndefinedPropertyAssignment(
'Static property ' . $property_id . ' is not defined',
new CodeLocation($statements_analyzer->getSource(), $stmt),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
$declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
$fq_class_name . '::$' . $prop_name->name,
false
);
$declaring_property_id = strtolower((string) $declaring_property_class) . '::$' . $prop_name;
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
);
if (!$moved_class) {
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern) {
list($old_declaring_fq_class_name) = explode('::$', $declaring_property_id);
list($new_fq_class_name, $new_property_name) = explode('::$', $transformation);
$file_manipulations = [];
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
}
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}
}
}
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
$property_storage = $class_storage->properties[$prop_name->name];
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
$class_property_type = $codebase->properties->getPropertyType(
$property_id,
true,
$statements_analyzer,
$context
);
if (!$class_property_type) {
$class_property_type = Type::getMixed();
if (!$assignment_value_type->hasMixed()) {
if ($property_storage->suggested_type) {
$property_storage->suggested_type = Type::combineUnionTypes(
$assignment_value_type,
$property_storage->suggested_type
);
} else {
$property_storage->suggested_type = Type::combineUnionTypes(
Type::getNull(),
$assignment_value_type
);
}
}
} else {
$class_property_type = clone $class_property_type;
}
if ($assignment_value_type->hasMixed()) {
return null;
}
if ($class_property_type->hasMixed()) {
return null;
}
$class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$class_property_type,
$fq_class_name,
$fq_class_name,
$class_storage->parent_class
);
$union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
$type_match_found = TypeAnalyzer::isContainedBy(
$codebase,
$assignment_value_type,
$class_property_type,
true,
true,
$union_comparison_results
);
if ($union_comparison_results->type_coerced) {
if ($union_comparison_results->type_coerced_from_mixed) {
if (IssueBuffer::accepts(
new MixedPropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type `' . $assignment_value_type->getId() . '` provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
} else {
if (IssueBuffer::accepts(
new PropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type \'' . $assignment_value_type->getId() . '\' provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
}
}
if ($union_comparison_results->to_string_cast) {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
$var_id . ' expects \'' . $class_property_type . '\', '
. '\'' . $assignment_value_type . '\' provided with a __toString method',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (!$type_match_found && !$union_comparison_results->type_coerced) {
if (TypeAnalyzer::canBeContainedBy($codebase, $assignment_value_type, $class_property_type)) {
if (IssueBuffer::accepts(
new PossiblyInvalidPropertyAssignmentValue(
$var_id . ' with declared type \''
. $class_property_type->getId() . '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new InvalidPropertyAssignmentValue(
$var_id . ' with declared type \'' . $class_property_type->getId()
. '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
}
}
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
return null;
}
}

View File

@ -5,7 +5,8 @@ use PhpParser;
use Psalm\Internal\Analyzer\CommentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\ArrayAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\PropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\StaticPropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
@ -824,7 +825,7 @@ class AssignmentAnalyzer
}
if ($prop_name) {
PropertyAssignmentAnalyzer::analyzeInstance(
InstancePropertyAssignmentAnalyzer::analyze(
$statements_analyzer,
$assign_var,
$prop_name,
@ -885,7 +886,7 @@ class AssignmentAnalyzer
}
if ($context->check_classes) {
PropertyAssignmentAnalyzer::analyzeStatic(
StaticPropertyAssignmentAnalyzer::analyze(
$statements_analyzer,
$assign_var,
$assign_value,
@ -1285,7 +1286,7 @@ class AssignmentAnalyzer
if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch && $stmt->name instanceof PhpParser\Node\Identifier) {
$prop_name = $stmt->name->name;
PropertyAssignmentAnalyzer::analyzeInstance(
InstancePropertyAssignmentAnalyzer::analyze(
$statements_analyzer,
$stmt,
$prop_name,

View File

@ -7,6 +7,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Taint\TaintNode;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\AbstractInstantiation;
@ -568,6 +569,32 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
$stmt_type->reference_free = true;
}
}
if ($codebase->taint
&& $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
&& ($stmt_type = $statements_analyzer->node_data->getType($stmt))
) {
$code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
if ($storage->external_mutation_free) {
$method_source = TaintNode::getForMethodReturn(
(string) $method_id,
$fq_class_name . '::__construct',
$storage->location,
$code_location
);
} else {
$method_source = TaintNode::getForMethodReturn(
(string) $method_id,
$fq_class_name . '::__construct',
$storage->location
);
}
$codebase->taint->addTaintNode($method_source);
$stmt_type->parent_nodes = [$method_source];
}
} else {
ArgumentsAnalyzer::analyze(
$statements_analyzer,
@ -579,6 +606,8 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
}
}
if (!$config->remember_property_assignments_after_call && !$context->collect_initializations) {
$context->removeAllObjectVars();
}

View File

@ -177,7 +177,13 @@ class InstancePropertyFetchAnalyzer
$property_id = $lhs_type_part->value . '::$' . $stmt->name->name;
self::processTaints($statements_analyzer, $stmt, $stmt_type, $property_id);
self::processTaints(
$statements_analyzer,
$stmt,
$stmt_type,
$property_id,
$codebase->classlike_storage_provider->get($lhs_type_part->value)
);
$codebase->properties->propertyExists(
$property_id,
@ -558,7 +564,13 @@ class InstancePropertyFetchAnalyzer
$statements_analyzer->node_data->setType($stmt, $stmt_type);
self::processTaints($statements_analyzer, $stmt, $stmt_type, $property_id);
self::processTaints(
$statements_analyzer,
$stmt,
$stmt_type,
$property_id,
$class_storage
);
continue;
}
@ -663,7 +675,13 @@ class InstancePropertyFetchAnalyzer
$statements_analyzer->node_data->setType($stmt, $stmt_type);
self::processTaints($statements_analyzer, $stmt, $stmt_type, $property_id);
self::processTaints(
$statements_analyzer,
$stmt,
$stmt_type,
$property_id,
$class_storage
);
continue;
}
@ -894,7 +912,13 @@ class InstancePropertyFetchAnalyzer
}
}
self::processTaints($statements_analyzer, $stmt, $class_property_type, $property_id);
self::processTaints(
$statements_analyzer,
$stmt,
$class_property_type,
$property_id,
$class_storage
);
if ($stmt_type = $statements_analyzer->node_data->getType($stmt)) {
$statements_analyzer->node_data->setType(
@ -1038,11 +1062,68 @@ class InstancePropertyFetchAnalyzer
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\PropertyFetch $stmt,
Type\Union $type,
string $property_id
string $property_id,
\Psalm\Storage\ClassLikeStorage $class_storage
) : void {
$codebase = $statements_analyzer->getCodebase();
if ($codebase->taint && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())) {
if (!$codebase->taint || !$codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())) {
return;
}
$var_location = new CodeLocation($statements_analyzer->getSource(), $stmt->var);
$property_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
if ($class_storage->specialize_instance) {
$var_id = ExpressionIdentifier::getArrayVarId(
$stmt->var,
null,
$statements_analyzer
);
$var_property_id = ExpressionIdentifier::getArrayVarId(
$stmt,
null,
$statements_analyzer
);
if ($var_id) {
$var_node = TaintNode::getForAssignment(
$var_id,
$var_location
);
$codebase->taint->addTaintNode($var_node);
$property_node = TaintNode::getForAssignment(
$var_property_id ?: $var_id . '->$property',
$property_location
);
$codebase->taint->addTaintNode($property_node);
$codebase->taint->addPath(
$var_node,
$property_node,
'property-fetch'
. ($stmt->name instanceof PhpParser\Node\Identifier ? '-' . $stmt->name : '')
);
$var_type = $statements_analyzer->node_data->getType($stmt->var);
if ($var_type && $var_type->parent_nodes) {
foreach ($var_type->parent_nodes as $parent_node) {
$codebase->taint->addPath(
$parent_node,
$var_node,
'='
);
}
}
$type->parent_nodes = [$property_node];
}
} else {
$code_location = new CodeLocation($statements_analyzer, $stmt->name);
$localized_property_node = new TaintNode(
@ -1065,7 +1146,7 @@ class InstancePropertyFetchAnalyzer
$codebase->taint->addPath($property_node, $localized_property_node, 'property-fetch');
$type->parent_nodes = [$localized_property_node];
$type->parent_nodes[] = $localized_property_node;
}
}
}

View File

@ -9,7 +9,7 @@ use Psalm\Internal\Analyzer\Statements\Block\IfAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\SwitchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\TryAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\WhileAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\PropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ClassConstFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
@ -445,7 +445,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
} elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) {
Statements\GlobalAnalyzer::analyze($statements_analyzer, $stmt, $context, $global_context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
PropertyAssignmentAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
InstancePropertyAssignmentAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
ClassConstFetchAnalyzer::analyzeClassConstAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) {

View File

@ -242,6 +242,14 @@ class Populator
}
}
if ($storage->specialize_instance) {
foreach ($storage->methods as $method) {
if (!$method->is_static) {
$method->specialize_call = true;
}
}
}
if ($storage->internal
&& !$storage->is_interface
&& !$storage->is_trait

View File

@ -247,6 +247,24 @@ class Taint
}
}
if (strpos($path_type, 'property-fetch-') === 0) {
$previous_path_types = array_reverse($generated_source->path_types);
foreach ($previous_path_types as $previous_path_type) {
if ($previous_path_type === 'property-assignment') {
break;
}
if (strpos($previous_path_type, 'property-assignment-') === 0) {
if (substr($previous_path_type, 20) === substr($path_type, 15)) {
break;
}
continue 2;
}
}
}
if (isset($sinks[$to_id])) {
$matching_taints = array_intersect($sinks[$to_id]->taints, $new_taints);

View File

@ -1350,6 +1350,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$storage->mutation_free = $docblock_info->mutation_free;
$storage->external_mutation_free = $docblock_info->external_mutation_free;
$storage->specialize_instance = $docblock_info->taint_specialize;
$storage->override_property_visibility = $docblock_info->override_property_visibility;
$storage->override_method_visibility = $docblock_info->override_method_visibility;

View File

@ -99,6 +99,11 @@ class ClassLikeDocblockComment
*/
public $external_mutation_free = false;
/**
* @var bool
*/
public $taint_specialize = false;
/**
* @var array<int, string>
*/

View File

@ -251,6 +251,11 @@ class ClassLikeStorage
*/
public $mutation_free = false;
/**
* @var bool
*/
public $specialize_instance = false;
/**
* @var array<lowercase-string, MethodStorage>
*/

View File

@ -1464,4 +1464,106 @@ class TaintTest extends TestCase
$this->analyzeFile('somefile.php', new Context());
}
public function testTaintPropertyPassingObject() : void
{
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('TaintedInput');
$this->project_analyzer->trackTaintedInputs();
$this->addFile(
'somefile.php',
'<?phps
/** @psalm-immutable */
class User {
public string $id;
public function __construct(string $userId) {
$this->id = $userId;
}
}
class UserUpdater {
public static function doDelete(PDO $pdo, User $user) : void {
self::deleteUser($pdo, $user->id);
}
public static function deleteUser(PDO $pdo, string $userId) : void {
$pdo->exec("delete from users where user_id = " . $userId);
}
}
$userObj = new User((string) $_GET["user_id"]);
UserUpdater::doDelete(new PDO(), $userObj);'
);
$this->analyzeFile('somefile.php', new Context());
}
public function testTaintPropertyPassingObjectWithDifferentValue() : void
{
$this->project_analyzer->trackTaintedInputs();
$this->addFile(
'somefile.php',
'<?phps
/** @psalm-immutable */
class User {
public string $id;
public $name = "Luke";
public function __construct(string $userId) {
$this->id = $userId;
}
}
class UserUpdater {
public static function doDelete(PDO $pdo, User $user) : void {
self::deleteUser($pdo, $user->name);
}
public static function deleteUser(PDO $pdo, string $userId) : void {
$pdo->exec("delete from users where user_id = " . $userId);
}
}
$userObj = new User((string) $_GET["user_id"]);
UserUpdater::doDelete(new PDO(), $userObj);'
);
$this->analyzeFile('somefile.php', new Context());
}
public function testTaintPropertyWithoutPassingObject() : void
{
$this->project_analyzer->trackTaintedInputs();
$this->addFile(
'somefile.php',
'<?phps
/** @psalm-immutable */
class User {
public string $id;
public function __construct(string $userId) {
$this->id = $userId;
}
}
class UserUpdater {
public static function doDelete(PDO $pdo, User $user) : void {
self::deleteUser($pdo, $user->id);
}
public static function deleteUser(PDO $pdo, string $userId) : void {
$pdo->exec("delete from users where user_id = " . $userId);
}
}
$userObj = new User((string) $_GET["user_id"]);'
);
$this->analyzeFile('somefile.php', new Context());
}
}