Skip to content
Permalink
Browse files

Add framework for taint analysis to Psalm

Ref #611
  • Loading branch information...
muglug committed Aug 4, 2019
1 parent ea1f987 commit b2c0993cdc023608c1ef7c4b010ae5a0354c6b8f
Showing with 1,298 additions and 83 deletions.
  1. +1 −0 config.xsd
  2. +4 −0 docs/running_psalm/issues.md
  3. +5 −0 src/Psalm/CodeLocation.php
  4. +7 −0 src/Psalm/Codebase.php
  5. +2 −0 src/Psalm/DocComment.php
  6. +18 −0 src/Psalm/Internal/Analyzer/CommentAnalyzer.php
  7. +1 −0 src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
  8. +11 −1 src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
  9. +9 −0 src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
  10. +40 −0 src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php
  11. +39 −0 src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
  12. +14 −0 src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php
  13. +10 −0 src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php
  14. +113 −14 src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
  15. +8 −0 src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
  16. +22 −0 src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/PropertyFetchAnalyzer.php
  17. +7 −0 src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
  18. +47 −7 src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php
  19. +2 −1 src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
  20. +81 −54 src/Psalm/Internal/Codebase/Analyzer.php
  21. +8 −3 src/Psalm/Internal/Codebase/Reflection.php
  22. +195 −0 src/Psalm/Internal/Codebase/Taint.php
  23. +10 −0 src/Psalm/Internal/Scanner/FunctionDocblockComment.php
  24. +33 −0 src/Psalm/Internal/Taint/TypeSource.php
  25. +20 −0 src/Psalm/Internal/Visitor/ReflectorVisitor.php
  26. +6 −0 src/Psalm/Issue/TaintedInput.php
  27. +1 −1 src/Psalm/Progress/Progress.php
  28. +10 −0 src/Psalm/Storage/FunctionLikeParameter.php
  29. +10 −0 src/Psalm/Type.php
  30. +14 −0 src/Psalm/Type/Union.php
  31. +6 −1 src/psalm.php
  32. +1 −0 tests/DocumentationTest.php
  33. +1 −1 tests/Progress/EchoProgress.php
  34. +534 −0 tests/TaintTest.php
  35. +8 −0 tests/TestCase.php
@@ -309,6 +309,7 @@
<xs:element name="RedundantConditionGivenDocblockType" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ReferenceConstraintViolation" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ReservedWord" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedInput" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TraitMethodSignatureMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TooFewArguments" type="ArgumentIssueHandlerType" minOccurs="0" />
<xs:element name="TooManyArguments" type="ArgumentIssueHandlerType" minOccurs="0" />
@@ -2044,6 +2044,10 @@ Emitted when using a reserved word as a class name
function foo(resource $res) : void {}
```

### TaintedInput

Emitted when tainted input detection is turned on

### TraitMethodSignatureMismatch

Emitted when a method's signature or return type differs from corresponding trait-defined method
@@ -404,4 +404,9 @@ public function getHash()
{
return (string) $this->file_start;
}
public function getShortSummary() : string
{
return $this->file_name . ':' . $this->getLineNumber() . ':' . $this->getColumn();
}
}
@@ -170,6 +170,11 @@ class Codebase
*/
public $populator;
/**
* @var ?Internal\Codebase\Taint
*/
public $taint = null;
/**
* @var bool
*/
@@ -267,6 +272,8 @@ class Codebase
*/
public $php_minor_version = PHP_MINOR_VERSION;
public function __construct(
Config $config,
Providers $providers,
@@ -150,6 +150,7 @@ public static function parse($docblock, $line_number = null, $preserve_format =
'override-method-visibility', 'seal-properties', 'seal-methods',
'generator-return', 'ignore-falsable-return', 'variadic', 'pure',
'ignore-variable-method', 'ignore-variable-property', 'internal',
'taint-sink', 'taint-source', 'assert-untainted',
],
true
)) {
@@ -271,6 +272,7 @@ public static function parsePreservingLength(\PhpParser\Comment\Doc $docblock)
'override-method-visibility', 'seal-properties', 'seal-methods',
'generator-return', 'ignore-falsable-return', 'variadic', 'pure',
'ignore-variable-method', 'ignore-variable-property', 'internal',
'taint-sink', 'taint-source', 'assert-untainted',
],
true
)) {
@@ -429,6 +429,24 @@ public static function extractFunctionDocblockInfo(PhpParser\Comment\Doc $commen
}
}
if (isset($parsed_docblock['specials']['psalm-taint-sink'])) {
/** @var string $param */
foreach ($parsed_docblock['specials']['psalm-taint-sink'] as $param) {
$param = trim($param);
$info->taint_sink_params[] = ['name' => $param];
}
}
if (isset($parsed_docblock['specials']['psalm-assert-untainted'])) {
/** @var string $param */
foreach ($parsed_docblock['specials']['psalm-assert-untainted'] as $param) {
$param = trim($param);
$info->assert_untainted_params[] = ['name' => $param];
}
}
if (isset($parsed_docblock['specials']['global'])) {
foreach ($parsed_docblock['specials']['global'] as $offset => $global) {
$line_parts = self::splitDocLine($global);
@@ -39,6 +39,7 @@
use function substr;
use function count;
use function in_array;
use Psalm\Internal\Taint\TypeSource;
/**
* @internal
@@ -42,6 +42,7 @@
use function array_keys;
use function end;
use function array_diff;
use Psalm\Internal\Taint\TypeSource;
/**
* @internal
@@ -267,7 +268,7 @@ public function analyze(
$context->calling_method_id = strtolower($method_id);
} elseif ($this->function instanceof Function_) {
$cased_method_id = $this->function->name;
$cased_method_id = $this->function->name->name;
} else { // Closure
if ($storage->return_type) {
$closure_return_type = ExpressionAnalyzer::fleshOutType(
@@ -591,6 +592,15 @@ function (FunctionLikeParameter $p) {
]);
}
if ($cased_method_id && $codebase->taint) {
$type_source = TypeSource::getForMethodArgument($cased_method_id, $offset, $function_param->location);
$var_type->sources = [$type_source];
if ($codebase->taint->hasExistingSource($type_source)) {
$var_type->tainted = 1;
}
}
$context->vars_in_scope['$' . $function_param->name] = $var_type;
$context->vars_possibly_in_scope['$' . $function_param->name] = true;
@@ -82,6 +82,7 @@
use function substr_count;
use function array_map;
use function end;
use Psalm\Internal\Codebase\Taint;
/**
* @internal
@@ -549,6 +550,14 @@ public function checkClassReferences()
);
}
/**
* @return void
*/
public function trackTaintedInputs()
{
$this->codebase->taint = new Taint();
}
public function interpretRefactors() : void
{
if (!$this->codebase->alter_code) {
@@ -41,6 +41,7 @@
use function in_array;
use function strtolower;
use function explode;
use Psalm\Internal\Taint\TypeSource;
/**
* @internal
@@ -455,6 +456,45 @@ public static function analyzeInstance(
}
}
if ($codebase->taint) {
$method_source = new TypeSource(
$property_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
);
if ($codebase->taint->hasPreviousSink($method_source)) {
if ($assignment_value_type->sources) {
$codebase->taint->addSinks(
$statements_analyzer,
$assignment_value_type->sources,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$method_source
);
}
}
if ($assignment_value_type->sources) {
foreach ($assignment_value_type->sources as $type_source) {
if ($codebase->taint->hasPreviousSource($type_source)
|| $assignment_value_type->tainted
) {
$codebase->taint->addSources(
$statements_analyzer,
[$method_source],
new CodeLocation($statements_analyzer->getSource(), $stmt),
$type_source
);
}
}
} elseif ($assignment_value_type->tainted) {
throw new \UnexpectedValueException(
'sources should exist for tainted var in '
. $statements_analyzer->getFileName() . ':'
. $stmt->getLine()
);
}
}
if (!$codebase->properties->propertyExists(
$property_id,
false,
@@ -329,6 +329,29 @@ function ($var_id) use ($original_vars_in_scope) {
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $context) === false) {
return false;
}
if ($codebase->taint) {
$sources = [];
$either_tainted = 0;
if (isset($stmt->left->inferredType)) {
$sources = $stmt->left->inferredType->sources ?: [];
$either_tainted = $stmt->left->inferredType->tainted;
}
if (isset($stmt->right->inferredType)) {
$sources = array_merge($sources, $stmt->right->inferredType->sources ?: []);
$either_tainted = $either_tainted | $stmt->right->inferredType->tainted;
}
if ($sources) {
$stmt->inferredType->sources = $sources;
}
if ($either_tainted) {
$stmt->inferredType->tainted = $either_tainted;
}
}
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
$t_if_context = clone $context;
@@ -578,6 +601,22 @@ function (\Psalm\Internal\Clause $c) use ($mixed_var_ids) {
if ($result_type) {
$stmt->inferredType = $result_type;
}
if ($codebase->taint && $stmt->inferredType) {
$sources = $stmt->left->inferredType->sources ?: [];
$either_tainted = $stmt->left->inferredType->tainted;
$sources = array_merge($sources, $stmt->right->inferredType->sources ?: []);
$either_tainted = $either_tainted | $stmt->right->inferredType->tainted;
if ($sources) {
$stmt->inferredType->sources = $sources;
}
if ($either_tainted) {
$stmt->inferredType->tainted = $either_tainted;
}
}
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr) {
self::analyzeNonDivArithmeticOp(
$statements_analyzer,
@@ -47,6 +47,7 @@
use function array_search;
use function array_keys;
use function in_array;
use Psalm\Internal\Taint\TypeSource;
/**
* @internal
@@ -1143,6 +1144,10 @@ function (PhpParser\Node\Arg $arg) {
$class_storage->parent_class
);
$return_type_candidate->sources = [
new TypeSource(strtolower($method_id), new CodeLocation($source, $stmt->name))
];
$return_type_location = $codebase->methods->getMethodReturnTypeLocation(
$method_id,
$secondary_return_type_location
@@ -1284,6 +1289,15 @@ function (Assertion $assertion) use ($class_template_params) : Assertion {
);
}
if ($codebase->taint && $method_id) {
$method_source = new TypeSource(strtolower($method_id), new CodeLocation($source, $stmt->name));
if ($codebase->taint->hasPreviousSource($method_source)) {
$return_type_candidate->tainted = 1;
$return_type_candidate->sources = [$method_source];
}
}
if (!$return_type) {
$return_type = $return_type_candidate;
} else {
@@ -31,6 +31,7 @@
use function is_string;
use function strlen;
use function substr;
use Psalm\Internal\Taint\TypeSource;
/**
* @internal
@@ -989,6 +990,15 @@ function (Assertion $assertion) use ($found_generic_params) : Assertion {
}
if ($return_type_candidate) {
if ($codebase->taint && $method_id) {
$method_source = new TypeSource(strtolower($method_id), new CodeLocation($source, $stmt->name));
if ($codebase->taint->hasPreviousSource($method_source)) {
$return_type_candidate->tainted = 1;
$return_type_candidate->sources = [$method_source];
}
}
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, $return_type_candidate);
} else {

0 comments on commit b2c0993

Please sign in to comment.
You can’t perform that action at this time.