Skip to content

Commit a2bdfd1

Browse files
committed
sprintf format string needs to be sanitized first
1 parent 8e583e9 commit a2bdfd1

31 files changed

+235
-123
lines changed

src/Internal/SprintfHelper.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Internal;
4+
5+
class SprintfHelper
6+
{
7+
8+
public static function escapeFormatString(string $format): string
9+
{
10+
return str_replace('%', '%%', $format);
11+
}
12+
13+
}

src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Arrays;
44

55
use PHPStan\Analyser\Scope;
6+
use PHPStan\Internal\SprintfHelper;
67
use PHPStan\Rules\RuleErrorBuilder;
78
use PHPStan\Rules\RuleLevelHelper;
89
use PHPStan\Type\ErrorType;
@@ -41,7 +42,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array
4142
{
4243
if ($node->dim !== null) {
4344
$dimType = $scope->getType($node->dim);
44-
$unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', $dimType->describe(VerbosityLevel::value()));
45+
$unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', SprintfHelper::escapeFormatString($dimType->describe(VerbosityLevel::value())));
4546
} else {
4647
$dimType = null;
4748
$unknownClassPattern = 'Access to an offset on an unknown class %s.';

src/Rules/AttributesCheck.php

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node\AttributeGroup;
66
use PhpParser\Node\Expr\New_;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Internal\SprintfHelper;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Reflection\ReflectionProvider;
1011

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

94+
$attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName());
95+
9396
$parameterErrors = $this->functionCallParametersCheck->check(
9497
ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()),
9598
$scope,
9699
$attributeConstructor->getDeclaringClass()->isBuiltin(),
97100
new New_($attribute->name, $attribute->args, $attribute->getAttributes()),
98101
[
99-
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d required.',
100-
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d required.',
101-
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.',
102-
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.',
103-
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.',
104-
'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.',
105-
'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor expects %s, %s given.',
102+
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.',
103+
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.',
104+
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.',
105+
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.',
106+
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.',
107+
'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.',
108+
'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.',
106109
'', // constructor does not have a return type
107-
'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor is passed by reference, so it expects variables only',
108-
'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClass->getDisplayName(),
109-
'Missing parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.',
110-
'Unknown parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.',
111-
'Return type of call to ' . $attributeClass->getDisplayName() . ' constructor contains unresolvable type.',
110+
'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only',
111+
'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName,
112+
'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.',
113+
'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.',
114+
'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.',
112115
]
113116
);
114117

src/Rules/Classes/ClassConstantRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr\ClassConstFetch;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Internal\SprintfHelper;
89
use PHPStan\Php\PhpVersion;
910
use PHPStan\Reflection\ReflectionProvider;
1011
use PHPStan\Rules\ClassCaseSensitivityCheck;
@@ -118,7 +119,7 @@ public function processNode(Node $node, Scope $scope): array
118119
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
119120
$scope,
120121
$class,
121-
sprintf('Access to constant %s on an unknown class %%s.', $constantName),
122+
sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)),
122123
static function (Type $type) use ($constantName): bool {
123124
return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes();
124125
}

src/Rules/Classes/InstantiationRule.php

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr\New_;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Internal\SprintfHelper;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Reflection\Php\PhpMethodReflection;
1011
use PHPStan\Reflection\ReflectionProvider;
@@ -175,6 +176,8 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $
175176
))->build();
176177
}
177178

179+
$classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName());
180+
178181
return array_merge($messages, $this->check->check(
179182
ParametersAcceptorSelector::selectFromArgs(
180183
$scope,
@@ -185,19 +188,19 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $
185188
$constructorReflection->getDeclaringClass()->isBuiltin(),
186189
$node,
187190
[
188-
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d required.',
189-
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d required.',
190-
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.',
191-
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.',
192-
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.',
193-
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.',
194-
'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor expects %s, %s given.',
191+
'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d required.',
192+
'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d required.',
193+
'Class ' . $classDisplayName . ' constructor invoked with %d parameter, at least %d required.',
194+
'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.',
195+
'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.',
196+
'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.',
197+
'Parameter %s of class ' . $classDisplayName . ' constructor expects %s, %s given.',
195198
'', // constructor does not have a return type
196-
'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor is passed by reference, so it expects variables only',
197-
'Unable to resolve the template type %s in instantiation of class ' . $classReflection->getDisplayName(),
198-
'Missing parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.',
199-
'Unknown parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.',
200-
'Return type of call to ' . $classReflection->getDisplayName() . ' constructor contains unresolvable type.',
199+
'Parameter %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only',
200+
'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName,
201+
'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.',
202+
'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.',
203+
'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.',
201204
]
202205
));
203206
}

src/Rules/Classes/UnusedConstructorParametersRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpParser\Node\Expr\Variable;
77
use PhpParser\Node\Param;
88
use PHPStan\Analyser\Scope;
9+
use PHPStan\Internal\SprintfHelper;
910
use PHPStan\Node\InClassMethodNode;
1011
use PHPStan\Reflection\MethodReflection;
1112
use PHPStan\Rules\UnusedFunctionParametersCheck;
@@ -50,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array
5051

5152
$message = sprintf(
5253
'Constructor of class %s has an unused parameter $%%s.',
53-
$scope->getClassReflection()->getDisplayName()
54+
SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName())
5455
);
5556
if ($scope->getClassReflection()->isAnonymous()) {
5657
$message = 'Constructor of an anonymous class has an unused parameter $%s.';

src/Rules/Functions/CallCallablesRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Functions;
44

55
use PHPStan\Analyser\Scope;
6+
use PHPStan\Internal\SprintfHelper;
67
use PHPStan\Reflection\InaccessibleMethod;
78
use PHPStan\Reflection\ParametersAcceptorSelector;
89
use PHPStan\Rules\FunctionCallParametersCheck;
@@ -104,7 +105,7 @@ static function (Type $type): bool {
104105
if ($type instanceof ClosureType) {
105106
$callableDescription = 'closure';
106107
} else {
107-
$callableDescription = sprintf('callable %s', $type->describe(VerbosityLevel::value()));
108+
$callableDescription = sprintf('callable %s', SprintfHelper::escapeFormatString($type->describe(VerbosityLevel::value())));
108109
}
109110

110111
return array_merge(

src/Rules/Functions/CallToFunctionParametersRule.php

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr\FuncCall;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Internal\SprintfHelper;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Reflection\ReflectionProvider;
1011
use PHPStan\Rules\FunctionCallParametersCheck;
@@ -41,6 +42,7 @@ public function processNode(Node $node, Scope $scope): array
4142
}
4243

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

4547
return $this->check->check(
4648
ParametersAcceptorSelector::selectFromArgs(
@@ -52,19 +54,19 @@ public function processNode(Node $node, Scope $scope): array
5254
$function->isBuiltin(),
5355
$node,
5456
[
55-
'Function ' . $function->getName() . ' invoked with %d parameter, %d required.',
56-
'Function ' . $function->getName() . ' invoked with %d parameters, %d required.',
57-
'Function ' . $function->getName() . ' invoked with %d parameter, at least %d required.',
58-
'Function ' . $function->getName() . ' invoked with %d parameters, at least %d required.',
59-
'Function ' . $function->getName() . ' invoked with %d parameter, %d-%d required.',
60-
'Function ' . $function->getName() . ' invoked with %d parameters, %d-%d required.',
61-
'Parameter %s of function ' . $function->getName() . ' expects %s, %s given.',
62-
'Result of function ' . $function->getName() . ' (void) is used.',
63-
'Parameter %s of function ' . $function->getName() . ' is passed by reference, so it expects variables only.',
64-
'Unable to resolve the template type %s in call to function ' . $function->getName(),
65-
'Missing parameter $%s in call to function ' . $function->getName() . '.',
66-
'Unknown parameter $%s in call to function ' . $function->getName() . '.',
67-
'Return type of call to function ' . $function->getName() . ' contains unresolvable type.',
57+
'Function ' . $functionName . ' invoked with %d parameter, %d required.',
58+
'Function ' . $functionName . ' invoked with %d parameters, %d required.',
59+
'Function ' . $functionName . ' invoked with %d parameter, at least %d required.',
60+
'Function ' . $functionName . ' invoked with %d parameters, at least %d required.',
61+
'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.',
62+
'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.',
63+
'Parameter %s of function ' . $functionName . ' expects %s, %s given.',
64+
'Result of function ' . $functionName . ' (void) is used.',
65+
'Parameter %s of function ' . $functionName . ' is passed by reference, so it expects variables only.',
66+
'Unable to resolve the template type %s in call to function ' . $functionName,
67+
'Missing parameter $%s in call to function ' . $functionName . '.',
68+
'Unknown parameter $%s in call to function ' . $functionName . '.',
69+
'Return type of call to function ' . $functionName . ' contains unresolvable type.',
6870
]
6971
);
7072
}

src/Rules/Functions/ExistingClassesInTypehintsRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Internal\SprintfHelper;
78
use PHPStan\Node\InFunctionNode;
89
use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
910
use PHPStan\Rules\FunctionDefinitionCheck;
@@ -32,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array
3233
return [];
3334
}
3435

35-
$functionName = $scope->getFunction()->getName();
36+
$functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName());
3637

3738
return $this->check->checkFunction(
3839
$node->getOriginalNode(),

src/Rules/Generics/ClassAncestorsRule.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Internal\SprintfHelper;
78
use PHPStan\Node\InClassNode;
89
use PHPStan\PhpDoc\Tag\ExtendsTag;
910
use PHPStan\PhpDoc\Tag\ImplementsTag;
@@ -69,38 +70,40 @@ public function processNode(Node $node, Scope $scope): array
6970
$implementsTags = $resolvedPhpDoc->getImplementsTags();
7071
}
7172

73+
$escapedClassName = SprintfHelper::escapeFormatString($className);
74+
7275
$extendsErrors = $this->genericAncestorsCheck->check(
7376
$originalNode->extends !== null ? [$originalNode->extends] : [],
7477
array_map(static function (ExtendsTag $tag): Type {
7578
return $tag->getType();
7679
}, $extendsTags),
77-
sprintf('Class %s @extends tag contains incompatible type %%s.', $className),
78-
sprintf('Class %s has @extends tag, but does not extend any class.', $className),
79-
sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $className),
80+
sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName),
81+
sprintf('Class %s has @extends tag, but does not extend any class.', $escapedClassName),
82+
sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $escapedClassName),
8083
'PHPDoc tag @extends contains generic type %s but class %s is not generic.',
8184
'Generic type %s in PHPDoc tag @extends does not specify all template types of class %s: %s',
8285
'Generic type %s in PHPDoc tag @extends specifies %d template types, but class %s supports only %d: %s',
8386
'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of class %s.',
8487
'PHPDoc tag @extends has invalid type %s.',
85-
sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $className),
86-
sprintf('in extended type %%s of class %s', $className)
88+
sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName),
89+
sprintf('in extended type %%s of class %s', $escapedClassName)
8790
);
8891

8992
$implementsErrors = $this->genericAncestorsCheck->check(
9093
$originalNode->implements,
9194
array_map(static function (ImplementsTag $tag): Type {
9295
return $tag->getType();
9396
}, $implementsTags),
94-
sprintf('Class %s @implements tag contains incompatible type %%s.', $className),
95-
sprintf('Class %s has @implements tag, but does not implement any interface.', $className),
96-
sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $className),
97+
sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName),
98+
sprintf('Class %s has @implements tag, but does not implement any interface.', $escapedClassName),
99+
sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $escapedClassName),
97100
'PHPDoc tag @implements contains generic type %s but interface %s is not generic.',
98101
'Generic type %s in PHPDoc tag @implements does not specify all template types of interface %s: %s',
99102
'Generic type %s in PHPDoc tag @implements specifies %d template types, but interface %s supports only %d: %s',
100103
'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of interface %s.',
101104
'PHPDoc tag @implements has invalid type %s.',
102-
sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $className),
103-
sprintf('in implemented type %%s of class %s', $className)
105+
sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName),
106+
sprintf('in implemented type %%s of class %s', $escapedClassName)
104107
);
105108

106109
foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) {

0 commit comments

Comments
 (0)