Skip to content

Commit

Permalink
sprintf format string needs to be sanitized first
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Sep 22, 2021
1 parent 8e583e9 commit a2bdfd1
Show file tree
Hide file tree
Showing 31 changed files with 235 additions and 123 deletions.
13 changes: 13 additions & 0 deletions src/Internal/SprintfHelper.php
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace PHPStan\Internal;

class SprintfHelper
{

public static function escapeFormatString(string $format): string
{
return str_replace('%', '%%', $format);
}

}
3 changes: 2 additions & 1 deletion src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Arrays;

use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\ErrorType;
Expand Down Expand Up @@ -41,7 +42,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array
{
if ($node->dim !== null) {
$dimType = $scope->getType($node->dim);
$unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', $dimType->describe(VerbosityLevel::value()));
$unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', SprintfHelper::escapeFormatString($dimType->describe(VerbosityLevel::value())));
} else {
$dimType = null;
$unknownClassPattern = 'Access to an offset on an unknown class %s.';
Expand Down
27 changes: 15 additions & 12 deletions src/Rules/AttributesCheck.php
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\New_;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;

Expand Down Expand Up @@ -90,25 +91,27 @@ public function check(
$errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name))->line($attribute->getLine())->build();
}

$attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName());

$parameterErrors = $this->functionCallParametersCheck->check(
ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()),
$scope,
$attributeConstructor->getDeclaringClass()->isBuiltin(),
new New_($attribute->name, $attribute->args, $attribute->getAttributes()),
[
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d required.',
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d required.',
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.',
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.',
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.',
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.',
'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor expects %s, %s given.',
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.',
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.',
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.',
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.',
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.',
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.',
'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.',
'', // constructor does not have a return type
'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor is passed by reference, so it expects variables only',
'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClass->getDisplayName(),
'Missing parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.',
'Unknown parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.',
'Return type of call to ' . $attributeClass->getDisplayName() . ' constructor contains unresolvable type.',
'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only',
'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName,
'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.',
'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.',
'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.',
]
);

Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Classes/ClassConstantRule.php
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ClassCaseSensitivityCheck;
Expand Down Expand Up @@ -118,7 +119,7 @@ public function processNode(Node $node, Scope $scope): array
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$class,
sprintf('Access to constant %s on an unknown class %%s.', $constantName),
sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)),
static function (Type $type) use ($constantName): bool {
return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes();
}
Expand Down
27 changes: 15 additions & 12 deletions src/Rules/Classes/InstantiationRule.php
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr\New_;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\PhpMethodReflection;
use PHPStan\Reflection\ReflectionProvider;
Expand Down Expand Up @@ -175,6 +176,8 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $
))->build();
}

$classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName());

return array_merge($messages, $this->check->check(
ParametersAcceptorSelector::selectFromArgs(
$scope,
Expand All @@ -185,19 +188,19 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $
$constructorReflection->getDeclaringClass()->isBuiltin(),
$node,
[
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.',
'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor expects %s, %s given.',
'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d required.',
'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d required.',
'Class ' . $classDisplayName . ' constructor invoked with %d parameter, at least %d required.',
'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.',
'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.',
'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.',
'Parameter %s of class ' . $classDisplayName . ' constructor expects %s, %s given.',
'', // constructor does not have a return type
'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor is passed by reference, so it expects variables only',
'Unable to resolve the template type %s in instantiation of class ' . $classReflection->getDisplayName(),
'Missing parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.',
'Unknown parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.',
'Return type of call to ' . $classReflection->getDisplayName() . ' constructor contains unresolvable type.',
'Parameter %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only',
'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName,
'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.',
'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.',
'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.',
]
));
}
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Classes/UnusedConstructorParametersRule.php
Expand Up @@ -6,6 +6,7 @@
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Param;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Rules\UnusedFunctionParametersCheck;
Expand Down Expand Up @@ -50,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array

$message = sprintf(
'Constructor of class %s has an unused parameter $%%s.',
$scope->getClassReflection()->getDisplayName()
SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName())
);
if ($scope->getClassReflection()->isAnonymous()) {
$message = 'Constructor of an anonymous class has an unused parameter $%s.';
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Functions/CallCallablesRule.php
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Functions;

use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\InaccessibleMethod;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\FunctionCallParametersCheck;
Expand Down Expand Up @@ -104,7 +105,7 @@ static function (Type $type): bool {
if ($type instanceof ClosureType) {
$callableDescription = 'closure';
} else {
$callableDescription = sprintf('callable %s', $type->describe(VerbosityLevel::value()));
$callableDescription = sprintf('callable %s', SprintfHelper::escapeFormatString($type->describe(VerbosityLevel::value())));
}

return array_merge(
Expand Down
28 changes: 15 additions & 13 deletions src/Rules/Functions/CallToFunctionParametersRule.php
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\FunctionCallParametersCheck;
Expand Down Expand Up @@ -41,6 +42,7 @@ public function processNode(Node $node, Scope $scope): array
}

$function = $this->reflectionProvider->getFunction($node->name, $scope);
$functionName = SprintfHelper::escapeFormatString($function->getName());

return $this->check->check(
ParametersAcceptorSelector::selectFromArgs(
Expand All @@ -52,19 +54,19 @@ public function processNode(Node $node, Scope $scope): array
$function->isBuiltin(),
$node,
[
'Function ' . $function->getName() . ' invoked with %d parameter, %d required.',
'Function ' . $function->getName() . ' invoked with %d parameters, %d required.',
'Function ' . $function->getName() . ' invoked with %d parameter, at least %d required.',
'Function ' . $function->getName() . ' invoked with %d parameters, at least %d required.',
'Function ' . $function->getName() . ' invoked with %d parameter, %d-%d required.',
'Function ' . $function->getName() . ' invoked with %d parameters, %d-%d required.',
'Parameter %s of function ' . $function->getName() . ' expects %s, %s given.',
'Result of function ' . $function->getName() . ' (void) is used.',
'Parameter %s of function ' . $function->getName() . ' is passed by reference, so it expects variables only.',
'Unable to resolve the template type %s in call to function ' . $function->getName(),
'Missing parameter $%s in call to function ' . $function->getName() . '.',
'Unknown parameter $%s in call to function ' . $function->getName() . '.',
'Return type of call to function ' . $function->getName() . ' contains unresolvable type.',
'Function ' . $functionName . ' invoked with %d parameter, %d required.',
'Function ' . $functionName . ' invoked with %d parameters, %d required.',
'Function ' . $functionName . ' invoked with %d parameter, at least %d required.',
'Function ' . $functionName . ' invoked with %d parameters, at least %d required.',
'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.',
'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.',
'Parameter %s of function ' . $functionName . ' expects %s, %s given.',
'Result of function ' . $functionName . ' (void) is used.',
'Parameter %s of function ' . $functionName . ' is passed by reference, so it expects variables only.',
'Unable to resolve the template type %s in call to function ' . $functionName,
'Missing parameter $%s in call to function ' . $functionName . '.',
'Unknown parameter $%s in call to function ' . $functionName . '.',
'Return type of call to function ' . $functionName . ' contains unresolvable type.',
]
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Functions/ExistingClassesInTypehintsRule.php
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\InFunctionNode;
use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
use PHPStan\Rules\FunctionDefinitionCheck;
Expand Down Expand Up @@ -32,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$functionName = $scope->getFunction()->getName();
$functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName());

return $this->check->checkFunction(
$node->getOriginalNode(),
Expand Down
23 changes: 13 additions & 10 deletions src/Rules/Generics/ClassAncestorsRule.php
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\InClassNode;
use PHPStan\PhpDoc\Tag\ExtendsTag;
use PHPStan\PhpDoc\Tag\ImplementsTag;
Expand Down Expand Up @@ -69,38 +70,40 @@ public function processNode(Node $node, Scope $scope): array
$implementsTags = $resolvedPhpDoc->getImplementsTags();
}

$escapedClassName = SprintfHelper::escapeFormatString($className);

$extendsErrors = $this->genericAncestorsCheck->check(
$originalNode->extends !== null ? [$originalNode->extends] : [],
array_map(static function (ExtendsTag $tag): Type {
return $tag->getType();
}, $extendsTags),
sprintf('Class %s @extends tag contains incompatible type %%s.', $className),
sprintf('Class %s has @extends tag, but does not extend any class.', $className),
sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $className),
sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName),
sprintf('Class %s has @extends tag, but does not extend any class.', $escapedClassName),
sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $escapedClassName),
'PHPDoc tag @extends contains generic type %s but class %s is not generic.',
'Generic type %s in PHPDoc tag @extends does not specify all template types of class %s: %s',
'Generic type %s in PHPDoc tag @extends specifies %d template types, but class %s supports only %d: %s',
'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of class %s.',
'PHPDoc tag @extends has invalid type %s.',
sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $className),
sprintf('in extended type %%s of class %s', $className)
sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName),
sprintf('in extended type %%s of class %s', $escapedClassName)
);

$implementsErrors = $this->genericAncestorsCheck->check(
$originalNode->implements,
array_map(static function (ImplementsTag $tag): Type {
return $tag->getType();
}, $implementsTags),
sprintf('Class %s @implements tag contains incompatible type %%s.', $className),
sprintf('Class %s has @implements tag, but does not implement any interface.', $className),
sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $className),
sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName),
sprintf('Class %s has @implements tag, but does not implement any interface.', $escapedClassName),
sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $escapedClassName),
'PHPDoc tag @implements contains generic type %s but interface %s is not generic.',
'Generic type %s in PHPDoc tag @implements does not specify all template types of interface %s: %s',
'Generic type %s in PHPDoc tag @implements specifies %d template types, but interface %s supports only %d: %s',
'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of interface %s.',
'PHPDoc tag @implements has invalid type %s.',
sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $className),
sprintf('in implemented type %%s of class %s', $className)
sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName),
sprintf('in implemented type %%s of class %s', $escapedClassName)
);

foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) {
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Generics/ClassTemplateTypeRule.php
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Type\Generic\TemplateTypeScope;
Expand Down Expand Up @@ -38,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array
if ($classReflection->isAnonymous()) {
$displayName = 'anonymous class';
} else {
$displayName = 'class ' . $classReflection->getDisplayName();
$displayName = 'class ' . SprintfHelper::escapeFormatString($classReflection->getDisplayName());
}

return $this->templateTypeCheck->check(
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Generics/FunctionSignatureVarianceRule.php
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\InFunctionNode;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\Rule;
Expand Down Expand Up @@ -37,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array

return $this->varianceCheck->checkParametersAcceptor(
ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()),
sprintf('in parameter %%s of function %s()', $functionName),
sprintf('in parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)),
sprintf('in return type of function %s()', $functionName),
sprintf('in function %s()', $functionName),
false
Expand Down
11 changes: 7 additions & 4 deletions src/Rules/Generics/FunctionTemplateTypeRule.php
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeScope;
Expand Down Expand Up @@ -52,14 +53,16 @@ public function processNode(Node $node, Scope $scope): array
$docComment->getText()
);

$escapedFunctionName = SprintfHelper::escapeFormatString($functionName);

return $this->templateTypeCheck->check(
$node,
TemplateTypeScope::createWithFunction($functionName),
$resolvedPhpDoc->getTemplateTags(),
sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $functionName),
sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $functionName),
sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $functionName),
sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $functionName)
sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $escapedFunctionName),
sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName),
sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName),
sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName)
);
}

Expand Down

0 comments on commit a2bdfd1

Please sign in to comment.