Skip to content

Commit

Permalink
Add detection to mixed params
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Aug 6, 2019
1 parent 8f6d432 commit 1775386
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 102 deletions.
262 changes: 160 additions & 102 deletions src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
Expand Up @@ -2280,6 +2280,19 @@ public static function checkFunctionArgumentType(
}
}

if ($cased_method_id) {
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$argument_offset,
$code_location,
$function_location,
$function_param,
$input_type,
$function_is_pure
);
}

return null;
}

Expand Down Expand Up @@ -2324,6 +2337,19 @@ public static function checkFunctionArgumentType(
);
}

if ($cased_method_id) {
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$argument_offset,
$code_location,
$function_location,
$function_param,
$input_type,
$function_is_pure
);
}

return null;
}

Expand Down Expand Up @@ -2374,113 +2400,22 @@ public static function checkFunctionArgumentType(
$input_type = $union_comparison_results->replacement_union_type;
}

if ($codebase->taint && $cased_method_id) {
$method_source = TypeSource::getForMethodArgument(
if ($cased_method_id) {
$old_input_type = $input_type;

self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$argument_offset,
$code_location
$code_location,
$function_location,
$function_param,
$input_type,
$function_is_pure
);

if (($function_param->is_sink || $codebase->taint->hasPreviousSink($method_source))
&& $input_type->sources
) {
$all_possible_sinks = [];

foreach ($input_type->sources as $source) {
if ($codebase->taint->hasExistingSink($source)) {
continue;
}

$all_possible_sinks[] = $source;

if (strpos($source->id, '::') && strpos($source->id, '#')) {
list($fq_classlike_name, $method_name) = explode('::', $source->id);

$method_name_parts = explode('#', $method_name);

$method_name = strtolower($method_name_parts[0]);

$class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name);

foreach ($class_storage->dependent_classlikes as $dependent_classlike => $_) {
$all_possible_sinks[] = TypeSource::getForMethodArgument(
$dependent_classlike . '::' . $method_name,
(int) $method_name_parts[1] - 1,
$code_location,
null
);
}

if (isset($class_storage->overridden_method_ids[$method_name])) {
foreach ($class_storage->overridden_method_ids[$method_name] as $parent_method_id) {
$all_possible_sinks[] = TypeSource::getForMethodArgument(
$parent_method_id,
(int) $method_name_parts[1] - 1,
$code_location,
null
);
}
}
}
}

$codebase->taint->addSinks(
$statements_analyzer,
$all_possible_sinks,
$code_location,
$method_source
);
}

if ($function_param->is_sink && $input_type->tainted) {
if (IssueBuffer::accepts(
new TaintedInput(
'in path ' . $codebase->taint->getPredecessorPath($method_source)
. ' out path ' . $codebase->taint->getSuccessorPath($method_source),
$code_location
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($input_type->sources) {
if ($function_is_pure) {
$codebase->taint->addSpecialization(
strtolower($cased_method_id . '#' . ($argument_offset + 1)),
$function_location->getShortSummary()
);

$method_source = TypeSource::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location,
$function_location
);
}

foreach ($input_type->sources as $type_source) {
if ($codebase->taint->hasPreviousSource($type_source) || $input_type->tainted) {
$codebase->taint->addSources(
$statements_analyzer,
[$method_source],
$code_location,
$type_source
);
}
}
} elseif ($input_type->tainted) {
throw new \UnexpectedValueException(
'sources should exist for tainted var in '
. $statements_analyzer->getFileName() . ':'
. $input_expr->getLine()
);
}

if ($function_param->assert_untainted) {
$input_type = clone $input_type;
if ($old_input_type !== $input_type) {
$replace_input_type = true;
$input_type->tainted = null;
$input_type->sources = [];
}
}

Expand Down Expand Up @@ -2818,6 +2753,129 @@ public static function checkFunctionArgumentType(
return null;
}

private static function processTaintedness(
StatementsAnalyzer $statements_analyzer,
string $cased_method_id,
int $argument_offset,
CodeLocation $code_location,
CodeLocation $function_location,
FunctionLikeParameter $function_param,
Type\Union &$input_type,
bool $function_is_pure
) : void {
$codebase = $statements_analyzer->getCodebase();

if (!$codebase->taint) {
return;
}

$method_source = TypeSource::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location
);

if (($function_param->is_sink || $codebase->taint->hasPreviousSink($method_source))
&& $input_type->sources
) {
$all_possible_sinks = [];

foreach ($input_type->sources as $source) {
if ($codebase->taint->hasExistingSink($source)) {
continue;
}

$all_possible_sinks[] = $source;

if (strpos($source->id, '::') && strpos($source->id, '#')) {
list($fq_classlike_name, $method_name) = explode('::', $source->id);

$method_name_parts = explode('#', $method_name);

$method_name = strtolower($method_name_parts[0]);

$class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name);

foreach ($class_storage->dependent_classlikes as $dependent_classlike => $_) {
$all_possible_sinks[] = TypeSource::getForMethodArgument(
$dependent_classlike . '::' . $method_name,
(int) $method_name_parts[1] - 1,
$code_location,
null
);
}

if (isset($class_storage->overridden_method_ids[$method_name])) {
foreach ($class_storage->overridden_method_ids[$method_name] as $parent_method_id) {
$all_possible_sinks[] = TypeSource::getForMethodArgument(
$parent_method_id,
(int) $method_name_parts[1] - 1,
$code_location,
null
);
}
}
}
}

$codebase->taint->addSinks(
$statements_analyzer,
$all_possible_sinks,
$code_location,
$method_source
);
}

if ($function_param->is_sink && $input_type->tainted) {
if (IssueBuffer::accepts(
new TaintedInput(
'in path ' . $codebase->taint->getPredecessorPath($method_source)
. ' out path ' . $codebase->taint->getSuccessorPath($method_source),
$code_location
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($input_type->sources) {
if ($function_is_pure) {
$codebase->taint->addSpecialization(
strtolower($cased_method_id . '#' . ($argument_offset + 1)),
$function_location->getShortSummary()
);

$method_source = TypeSource::getForMethodArgument(
$cased_method_id,
$argument_offset,
$code_location,
$function_location
);
}

foreach ($input_type->sources as $type_source) {
if ($codebase->taint->hasPreviousSource($type_source) || $input_type->tainted) {
$codebase->taint->addSources(
$statements_analyzer,
[$method_source],
$code_location,
$type_source
);
}
}
} elseif ($input_type->tainted) {
throw new \UnexpectedValueException(
'sources should exist for tainted var in '
. $code_location->getShortSummary()
);
}

if ($function_param->assert_untainted) {
$input_type = clone $input_type;
$input_type->tainted = null;
$input_type->sources = [];
}
}

private static function coerceValueAfterGatekeeperArgument(
StatementsAnalyzer $statements_analyzer,
Type\Union $input_type,
Expand Down
23 changes: 23 additions & 0 deletions tests/TaintTest.php
Expand Up @@ -690,4 +690,27 @@ function getAppendedUserId() : void {

$this->analyzeFile('somefile.php', new Context());
}

public function testTaintOverMixed() : void
{
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('TaintedInput');

$this->project_analyzer->trackTaintedInputs();

$this->addFile(
'somefile.php',
'<?php
/**
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedArgument
*/
function foo() : void {
$a = $_GET["bad"];
echo $a;
}'
);

$this->analyzeFile('somefile.php', new Context());
}
}

0 comments on commit 1775386

Please sign in to comment.