Skip to content

Commit 271f837

Browse files
authored
support non-conditional @psalm-taint-escape sql (#437)
* support `@psalm-taint-escape sql` * cs * cs * Update PhpDocUtil.php
1 parent 59d5d8e commit 271f837

File tree

4 files changed

+85
-14
lines changed

4 files changed

+85
-14
lines changed

src/PhpDoc/PhpDocUtil.php

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,43 @@ final class PhpDocUtil
1313
{
1414
/**
1515
* @api
16+
*
17+
* @param CallLike|MethodReflection $callLike
18+
*/
19+
public static function matchTaintEscape($callLike, Scope $scope): ?string
20+
{
21+
if ($callLike instanceof CallLike) {
22+
$methodReflection = self::getMethodReflection($callLike, $scope);
23+
} else {
24+
$methodReflection = $callLike;
25+
}
26+
27+
// XXX does not yet support conditional escaping
28+
// https://psalm.dev/docs/security_analysis/avoiding_false_positives/#conditional-escaping-tainted-input
29+
if (null !== $methodReflection) {
30+
// atm no resolved phpdoc for methods
31+
// see https://github.com/phpstan/phpstan/discussions/7657
32+
$phpDocString = $methodReflection->getDocComment();
33+
if (null !== $phpDocString && preg_match('/@psalm-taint-escape\s+(\S+)$/m', $phpDocString, $matches)) {
34+
return $matches[1];
35+
}
36+
}
37+
38+
return null;
39+
}
40+
41+
/**
42+
* @param CallLike|MethodReflection $callLike
43+
*
44+
*@api
1645
*/
17-
public static function commentContains(string $text, CallLike $callike, Scope $scope): bool
46+
public static function commentContains(string $text, $callLike, Scope $scope): bool
1847
{
19-
$methodReflection = self::getMethodReflection($callike, $scope);
48+
if ($callLike instanceof CallLike) {
49+
$methodReflection = self::getMethodReflection($callLike, $scope);
50+
} else {
51+
$methodReflection = $callLike;
52+
}
2053

2154
if (null !== $methodReflection) {
2255
// atm no resolved phpdoc for methods
@@ -35,9 +68,9 @@ public static function commentContains(string $text, CallLike $callike, Scope $s
3568
*
3669
* @param string $annotation e.g. '@phpstandba-inference-placeholder'
3770
*/
38-
public static function matchStringAnnotation(string $annotation, CallLike $callike, Scope $scope): ?string
71+
public static function matchStringAnnotation(string $annotation, CallLike $callLike, Scope $scope): ?string
3972
{
40-
$methodReflection = self::getMethodReflection($callike, $scope);
73+
$methodReflection = self::getMethodReflection($callLike, $scope);
4174

4275
if (null !== $methodReflection) {
4376
// atm no resolved phpdoc for methods
@@ -57,21 +90,21 @@ public static function matchStringAnnotation(string $annotation, CallLike $calli
5790
return null;
5891
}
5992

60-
private static function getMethodReflection(CallLike $callike, Scope $scope): ?MethodReflection
93+
private static function getMethodReflection(CallLike $callLike, Scope $scope): ?MethodReflection
6194
{
6295
$methodReflection = null;
63-
if ($callike instanceof Expr\StaticCall) {
64-
if ($callike->class instanceof Name && $callike->name instanceof Identifier) {
65-
$classType = $scope->resolveTypeByName($callike->class);
66-
$methodReflection = $scope->getMethodReflection($classType, $callike->name->name);
96+
if ($callLike instanceof Expr\StaticCall) {
97+
if ($callLike->class instanceof Name && $callLike->name instanceof Identifier) {
98+
$classType = $scope->resolveTypeByName($callLike->class);
99+
$methodReflection = $scope->getMethodReflection($classType, $callLike->name->name);
67100
}
68-
} elseif ($callike instanceof Expr\MethodCall && $callike->name instanceof Identifier) {
101+
} elseif ($callLike instanceof Expr\MethodCall && $callLike->name instanceof Identifier) {
69102
$classReflection = $scope->getClassReflection();
70-
if (null !== $classReflection && $classReflection->hasMethod($callike->name->name)) {
71-
$methodReflection = $classReflection->getMethod($callike->name->name, $scope);
103+
if (null !== $classReflection && $classReflection->hasMethod($callLike->name->name)) {
104+
$methodReflection = $classReflection->getMethod($callLike->name->name, $scope);
72105
} else {
73-
$callerType = $scope->getType($callike->var);
74-
$methodReflection = $scope->getMethodReflection($callerType, $callike->name->name);
106+
$callerType = $scope->getType($callLike->var);
107+
$methodReflection = $scope->getMethodReflection($callerType, $callLike->name->name);
75108
}
76109
}
77110

src/QueryReflection/QueryReflection.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ private function resolveQueryStringExpr(Expr $queryExpr, Scope $scope, bool $res
201201
}
202202

203203
if ($queryExpr instanceof Expr\CallLike) {
204+
if ('sql' === PhpDocUtil::matchTaintEscape($queryExpr, $scope)) {
205+
return '1';
206+
}
207+
204208
$placeholder = PhpDocUtil::matchStringAnnotation('@phpstandba-inference-placeholder', $queryExpr, $scope);
205209

206210
if (null !== $placeholder) {

tests/default/Fixture/Escaper.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace staabm\PHPStanDba\Tests\Fixture;
4+
5+
final class Escaper
6+
{
7+
/**
8+
* @psalm-taint-escape sql
9+
*/
10+
public function escape($s): string
11+
{
12+
}
13+
14+
/**
15+
* @psalm-taint-escape sql
16+
*/
17+
public static function staticEscape($s): string
18+
{
19+
}
20+
}

tests/default/data/pdo.php

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

55
use PDO;
66
use function PHPStan\Testing\assertType;
7+
use staabm\PHPStanDba\Tests\Fixture\Escaper;
78

89
class Foo
910
{
@@ -224,4 +225,17 @@ public function queryEncapsedString(PDO $pdo, int $adaid)
224225
$stmt = $pdo->query("SELECT email, adaid FROM ada WHERE adaid={$fn()}", PDO::FETCH_ASSOC);
225226
assertType('PDOStatement<array{email: string, adaid: int<-32768, 32767>}>', $stmt);
226227
}
228+
229+
public function taintStaticEscaped(PDO $pdo, string $s)
230+
{
231+
$stmt = $pdo->query('SELECT email, adaid FROM ada WHERE adaid='.Escaper::staticEscape($s), PDO::FETCH_ASSOC);
232+
assertType('PDOStatement<array{email: string, adaid: int<-32768, 32767>}>', $stmt);
233+
}
234+
235+
public function taintEscaped(PDO $pdo, string $s)
236+
{
237+
$escapeer = new Escaper();
238+
$stmt = $pdo->query('SELECT email, adaid FROM ada WHERE adaid='.$escapeer->escape($s), PDO::FETCH_ASSOC);
239+
assertType('PDOStatement<array{email: string, adaid: int<-32768, 32767>}>', $stmt);
240+
}
227241
}

0 commit comments

Comments
 (0)