getTestName(); if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile( $file_path, $code, ); $this->project_analyzer->trackTaintedInputs(); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $this->analyzeFile($file_path, new Context(), false); } /** * @dataProvider providerInvalidCodeParse */ public function testInvalidCode(string $code, string $error_message): void { if (strpos($this->getTestName(), 'SKIPPED-') !== false) { $this->markTestSkipped(); } $this->expectException(CodeException::class); $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/'); $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile( $file_path, $code, ); $this->project_analyzer->trackTaintedInputs(); $this->analyzeFile($file_path, new Context(), false); } /** * @return array */ public function providerValidCodeParse(): array { return [ 'taintedInputInCreatedArrayNotEchoed' => [ 'code' => ' $name, "id" => $id]; echo "

" . htmlentities($data["name"], \ENT_QUOTES) . "

"; echo "

" . $data["id"] . "

";', ], 'taintedInputInAssignedArrayNotEchoed' => [ 'code' => '" . htmlentities($data["name"], \ENT_QUOTES) . ""; echo "

" . $data["id"] . "

";', ], 'taintedInputDirectlySuppressed' => [ 'code' => 'exec("delete from users where user_id = " . $userId); } }', ], 'taintedInputDirectlySuppressedWithOtherUse' => [ 'code' => 'exec("delete from users where user_id = " . $userId); } public function deleteUserSafer(PDOWrapper $pdo) : void { $userId = $this->getSafeId(); $pdo->exec("delete from users where user_id = " . $userId); } public function getSafeId() : string { return "5"; } } class PDOWrapper { /** * @psalm-taint-sink sql $sql */ public function exec(string $sql) : void {} }', ], 'taintedInputToParamButSafe' => [ 'code' => 'deleteUser( $pdo, $this->getAppendedUserId((string) $_GET["user_id"]) ); } public function getAppendedUserId(string $user_id) : string { return "aaa" . $user_id; } public function deleteUser(PDO $pdo, string $userId) : void { $userId2 = strlen($userId); $pdo->exec("delete from users where user_id = " . $userId2); } }', ], 'ValidatedInputFromParam' => [ 'code' => 'getUserId(); validateUserId($userId); $this->deleteUser($pdo, $userId); } public function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } }', ], 'untaintedInputAfterIntCast' => [ 'code' => 'getUserId(); } public function deleteUser(PDO $pdo) : void { $userId = $this->getAppendedUserId(); $pdo->exec("delete from users where user_id = " . $userId); } }', ], 'specializedCoreFunctionCall' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 's = (string) $_GET["FOO"]; } } class V1 extends V { public function foo(O1 $o) : void { echo U::shorten($o->s); } }', ], 'taintOnPregReplaceCallRemovedInFunction' => [ 'code' => 's = (string) $_GET["FOO"]; } } class V1 extends V { public function foo(O1 $o) : void { echo U::shorten($o->s); } }', ], 'taintOnStrReplaceCallRemovedInline' => [ 'code' => 's = (string) $_GET["FOO"]; } } class V1 extends V { public function foo(O1 $o) : void { /** * @psalm-taint-escape html * @psalm-taint-escape has_quotes */ $a = str_replace("foo", "bar", $o->s); echo $a; } }', ], 'NoTaintsOnSimilarPureCall' => [ 'code' => 's = $s; } } class O2 { public string $t; public function __construct() { $this->t = (string) $_GET["FOO"]; } } class V1 { public function foo() : void { $o = new O1((string) $_GET["FOO"]); echo U::escape(U::shorten($o->s)); } } class V2 { public function foo(O2 $o) : void { echo U::shorten(U::escape($o->t)); } }', ], 'taintPropertyPassingObjectWithDifferentValue' => [ 'code' => 'id = $userId; } } class UserUpdater { public static function doDelete(PDO $pdo, User $user) : void { self::deleteUser($pdo, $user->name); } public static function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } } $userObj = new User((string) $_GET["user_id"]); UserUpdater::doDelete(new PDO(), $userObj);', ], 'taintPropertyWithoutPassingObject' => [ 'code' => 'id = $userId; } public function setId(string $userId) : void { $this->id = $userId; } } function echoId(User $u2) : void { echo $u2->id; } $u = new User("5"); echoId($u); $u->setId($_GET["user_id"]);', ], 'specializeStaticMethod' => [ 'code' => ' [ 'code' => ' $_GET["name"], "b" => "foo"]; foreach ($a as $m) { echo $m["b"]; }', ], 'taintFreeNestedArrayWithOffsetAccessedExplicitly' => [ 'code' => ' $_GET["name"], "b" => "foo"]; echo $a[0]["b"];', ], 'dontTaintSpecializedInstanceProperty' => [ 'code' => 'x = $x; } } $a = new StringHolder("a"); $b = new StringHolder($_GET["x"]); echo $a->x;', ], 'dontTaintSpecializedCallsForAnonymousInstance' => [ 'code' => 'render($_GET["untrusted"]); echo (new StringRenderer())->render("a");', ], 'dontTaintSpecializedCallsForStubMadeInstance' => [ 'code' => 'render($_GET["untrusted"]); echo stub()->render("a");', ], 'suppressTaintedInput' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } class B extends A { public function __construct($taint) {} } $b = new B($_GET["bar"]); echo $b->getTaint();', ], 'immutableClassTrackInputThroughMethod' => [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } $b = new A($_GET["bar"]); $a = new A("bar"); echo $a->getTaint();', ], 'literalStringCannotCarryTaint' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $_) { echo $key; } } takesArray(["good" => $_GET["bad"]]);', ], 'resultOfComparisonIsNotTainted' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [["c" => "hello"]], "b" => [], ]; $foo["b"][] = [ "c" => $_GET["bad"], ]; bar($foo["a"]); } function bar(array $arr): void { foreach ($arr as $s) { echo $s["c"]; } }', ], 'urlencode' => [ 'code' => ' [ 'code' => 'escape_string($_GET["a"]); $b = mysqli_escape_string($_GET["b"]); $c = $mysqli->real_escape_string($_GET["c"]); $d = mysqli_real_escape_string($_GET["d"]); $mysqli->query("$a$b$c$d");', ], ]; } /** * @return array */ public function providerInvalidCodeParse(): array { return [ 'taintedInputFromMethodReturnTypeSimple' => [ 'code' => 'getUserId(); } public function deleteUser(PDO $pdo) : void { $userId = $this->getAppendedUserId(); $pdo->exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql', ], 'taintedInputFromFunctionReturnType' => [ 'code' => ' 'TaintedHtml - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:26 - Detected tainted HTML in path: $_GET -> $_GET[\'name\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:3:32) -> coalesce (src' . DIRECTORY_SEPARATOR . 'somefile.php:3:32) -> getName (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:42) -> call to echo (src' . DIRECTORY_SEPARATOR . 'somefile.php:6:26) -> echo#1', ], 'taintedInputFromExplicitTaintSource' => [ 'code' => ' 'TaintedHtml', ], 'taintedInputFromExplicitTaintSourceStaticMethod' => [ 'code' => ' 'TaintedHtml', ], 'taintedInputFromGetArray' => [ 'code' => ' 'TaintedHtml', ], 'taintedInputFromReturnToInclude' => [ 'code' => ' 'TaintedInclude', ], 'taintedInputFromReturnToEval' => [ 'code' => ' 'TaintedEval', ], 'taintedInputFromReturnTypeToEcho' => [ 'code' => 'getUserId(); } public function deleteUser(PDO $pdo) : void { $userId = $this->getAppendedUserId(); echo $userId; } }', 'error_message' => 'TaintedHtml', ], 'taintedInputInCreatedArrayIsEchoed' => [ 'code' => ' $name]; echo "

" . $data["name"] . "

";', 'error_message' => 'TaintedHtml', ], 'testTaintedInputInAssignedArrayIsEchoed' => [ 'code' => '" . $data["name"] . "";', 'error_message' => 'TaintedHtml', ], 'taintedInputDirectly' => [ 'code' => 'exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql', ], 'taintedInputFromReturnTypeWithBranch' => [ 'code' => 'getUserId(); if (rand(0, 1)) { $userId .= "aaa"; } else { $userId .= "bb"; } return $userId; } public function deleteUser(PDO $pdo) : void { $userId = $this->getAppendedUserId(); $pdo->exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql', ], 'sinkAnnotation' => [ 'code' => 'getUserId(); } public function deleteUser(PDOWrapper $pdo) : void { $userId = $this->getAppendedUserId(); $pdo->exec("delete from users where user_id = " . $userId); } } class PDOWrapper { /** * @psalm-taint-sink sql $sql */ public function exec(string $sql) : void {} }', 'error_message' => 'TaintedSql', ], 'taintedInputFromParam' => [ 'code' => 'getUserId(); } public function doDelete(PDO $pdo) : void { $userId = $this->getAppendedUserId(); $this->deleteUser($pdo, $userId); } public function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql - src' . DIRECTORY_SEPARATOR . 'somefile.php:17:40 - Detected tainted SQL in path: $_GET -> $_GET[\'user_id\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:4:45) -> A::getUserId (src' . DIRECTORY_SEPARATOR . 'somefile.php:3:55) -> concat (src' . DIRECTORY_SEPARATOR . 'somefile.php:8:36) -> A::getAppendedUserId (src' . DIRECTORY_SEPARATOR . 'somefile.php:7:63) -> $userId (src' . DIRECTORY_SEPARATOR . 'somefile.php:12:29) -> call to A::deleteUser (src' . DIRECTORY_SEPARATOR . 'somefile.php:13:53) -> $userId (src' . DIRECTORY_SEPARATOR . 'somefile.php:16:69) -> call to PDO::exec (src' . DIRECTORY_SEPARATOR . 'somefile.php:17:40) -> PDO::exec#1', ], 'taintedInputToParam' => [ 'code' => 'deleteUser( $pdo, $this->getAppendedUserId((string) $_GET["user_id"]) ); } public function getAppendedUserId(string $user_id) : string { return "aaa" . $user_id; } public function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql', ], 'taintedInputToParamAfterAssignment' => [ 'code' => 'deleteUser( $pdo, $this->getAppendedUserId((string) $_GET["user_id"]) ); } public function getAppendedUserId(string $user_id) : string { return "aaa" . $user_id; } public function deleteUser(PDO $pdo, string $userId) : void { $userId2 = $userId; $pdo->exec("delete from users where user_id = " . $userId2); } }', 'error_message' => 'TaintedSql', ], 'taintedInputToParamAlternatePath' => [ 'code' => 'deleteUser( $pdo, self::doFoo(), $this->getAppendedUserId((string) $_GET["user_id"]) ); } public function getAppendedUserId(string $user_id) : string { return "aaa" . $user_id; } public static function doFoo() : string { return "hello"; } public function deleteUser(PDO $pdo, string $userId, string $userId2) : void { $pdo->exec("delete from users where user_id = " . $userId); if (rand(0, 1)) { $pdo->exec("delete from users where user_id = " . $userId2); } } }', 'error_message' => 'TaintedSql - src' . DIRECTORY_SEPARATOR . 'somefile.php:23:44 - Detected tainted SQL in path: $_GET -> $_GET[\'user_id\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:7:67) -> call to A::getAppendedUserId (src' . DIRECTORY_SEPARATOR . 'somefile.php:7:58) -> $user_id (src' . DIRECTORY_SEPARATOR . 'somefile.php:11:66) -> concat (src' . DIRECTORY_SEPARATOR . 'somefile.php:12:36) -> A::getAppendedUserId (src' . DIRECTORY_SEPARATOR . 'somefile.php:11:78) -> call to A::deleteUser (src' . DIRECTORY_SEPARATOR . 'somefile.php:7:33) -> $userId2 (src' . DIRECTORY_SEPARATOR . 'somefile.php:19:85) -> call to PDO::exec (src' . DIRECTORY_SEPARATOR . 'somefile.php:23:44) -> PDO::exec#1', ], 'taintedInParentLoader' => [ 'code' => 'exec("select * from foo where bar = " . $sink); } } class AGrandChild extends AChild {} class C { public function foo(string $user_id) : void { AGrandChild::loadFull($user_id); } } (new C)->foo((string) $_GET["user_id"]);', 'error_message' => 'TaintedSql - src' . DIRECTORY_SEPARATOR . 'somefile.php:16:44 - Detected tainted SQL in path: $_GET -> $_GET[\'user_id\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:28:43) -> call to C::foo (src' . DIRECTORY_SEPARATOR . 'somefile.php:28:34) -> $user_id (src' . DIRECTORY_SEPARATOR . 'somefile.php:23:52) -> call to AGrandChild::loadFull (src' . DIRECTORY_SEPARATOR . 'somefile.php:24:51) -> AGrandChild::loadFull#1 (src' . DIRECTORY_SEPARATOR . 'somefile.php:5:64) -> A::loadFull#1 (src' . DIRECTORY_SEPARATOR . 'somefile.php:24:51) -> $sink (src' . DIRECTORY_SEPARATOR . 'somefile.php:5:64) -> call to A::loadPartial (src' . DIRECTORY_SEPARATOR . 'somefile.php:6:49) -> A::loadPartial#1 (src' . DIRECTORY_SEPARATOR . 'somefile.php:3:76) -> AChild::loadPartial#1 (src' . DIRECTORY_SEPARATOR . 'somefile.php:6:49) -> $sink (src' . DIRECTORY_SEPARATOR . 'somefile.php:15:67) -> call to PDO::exec (src' . DIRECTORY_SEPARATOR . 'somefile.php:16:44) -> PDO::exec#1', ], 'taintedInputFromProperty' => [ 'code' => 'userId = (string) $_GET["user_id"]; } public function getAppendedUserId() : string { return "aaaa" . $this->userId; } public function doDelete(PDO $pdo) : void { $userId = $this->getAppendedUserId(); $this->deleteUser($pdo, $userId); } public function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql', ], 'taintedInputFromPropertyViaMixin' => [ 'code' => 'userId = (string) $_GET["user_id"]; } } /** @mixin A */ class B { private A $a; public function __construct(A $a) { $this->a = $a; } public function __get(string $name) { return $this->a->$name; } } class C { private B $b; public function __construct(B $b) { $this->b = $b; } public function getAppendedUserId() : string { return "aaaa" . $this->b->userId; } public function doDelete(PDO $pdo) : void { $userId = $this->getAppendedUserId(); $this->deleteUser($pdo, $userId); } public function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } }', 'error_message' => 'TaintedSql', ], 'taintedInputViaStaticFunction' => [ 'code' => ' 'TaintedHtml', ], 'taintedInputViaPureStaticFunction' => [ 'code' => ' 'TaintedHtml', ], 'untaintedInputViaStaticFunctionWithoutSafePath' => [ 'code' => ' 'TaintedHtml', ], 'taintedInputFromMagicProperty' => [ 'code' => ' */ private $vars = []; public function __get(string $s) : string { return $this->vars[$s]; } public function __set(string $s, string $t) { $this->vars[$s] = $t; } } function getAppendedUserId() : void { $a = new A(); $a->userId = (string) $_GET["user_id"]; echo $a->userId; }', 'error_message' => 'TaintedHtml', ], 'taintOverMixed' => [ 'code' => ' 'TaintedHtml', ], 'taintStrConversion' => [ 'code' => ' 'TaintedHtml', ], 'taintIntoExec' => [ 'code' => ' 'TaintedShell', ], 'taintIntoExecMultipleConcat' => [ 'code' => ' 'TaintedShell', ], 'taintIntoNestedArrayUnnestedSeparately' => [ 'code' => ' 'TaintedShell', ], 'taintIntoArrayAndThenOutAgain' => [ 'code' => ' 'TaintedShell', ], 'taintAppendedToArray' => [ 'code' => ' 'TaintedShell', ], 'taintOnSubstrCall' => [ 'code' => 's = (string) $_GET["FOO"]; } } class V1 extends V { public function foo(O1 $o) : void { echo U::shorten($o->s); } }', 'error_message' => 'TaintedHtml', ], 'taintOnStrReplaceCallSimple' => [ 'code' => 's = (string) $_GET["FOO"]; } } class V1 extends V { public function foo(O1 $o) : void { echo U::shorten($o->s); } }', 'error_message' => 'TaintedHtml', ], 'taintOnPregReplaceCall' => [ 'code' => 's = (string) $_GET["FOO"]; } } class V1 extends V { public function foo(O1 $o) : void { echo U::shorten($o->s); } }', 'error_message' => 'TaintedHtml', ], 'IndirectGetAssignment' => [ 'code' => 'name = $name; } /** * @psalm-specialize-call */ public function getArg(string $method, string $type) { $arg = null; switch ($method) { case "post": if (isset($_POST[$this->name])) { $arg = $_POST[$this->name]; } break; case "get": if (isset($_GET[$this->name])) { $arg = $_GET[$this->name]; } break; } return $this->filterInput($type, $arg); } protected function filterInput(string $type, $arg) { // input is null if ($arg === null) { return null; } // set to null if sanitize clears arg if ($arg === "") { $arg = null; } // type casting if ($arg !== null) { $arg = $this->typeCastInput($type, $arg); } return $arg; } protected function typeCastInput(string $type, $arg) { if ($type === "string") { return (string) $arg; } return null; } } echo (new InputFilter("hello"))->getArg("get", "string");', 'error_message' => 'TaintedHtml', ], 'taintPropertyPassingObject' => [ 'code' => 'id = $userId; } } class UserUpdater { public static function doDelete(PDO $pdo, User $user) : void { self::deleteUser($pdo, $user->id); } public static function deleteUser(PDO $pdo, string $userId) : void { $pdo->exec("delete from users where user_id = " . $userId); } } $userObj = new User((string) $_GET["user_id"]); UserUpdater::doDelete(new PDO(), $userObj);', 'error_message' => 'TaintedSql', ], 'taintPropertyPassingObjectSettingValueLater' => [ 'code' => 'id = $userId; } public function setId(string $userId) : void { $this->id = $userId; } } function echoId(User $u2) : void { echo $u2->id; } $u = new User("5"); $u->setId($_GET["user_id"]); echoId($u);', 'error_message' => 'TaintedHtml', ], 'ImplodeExplode' => [ 'code' => ' 'TaintedHtml', ], 'ImplodeIndirect' => [ 'code' => ' 'TaintedHtml', ], 'taintThroughPregReplaceCallback' => [ 'code' => ' 'TaintedHtml', ], 'taintedFunctionWithNoTypes' => [ 'code' => ' 'TaintedHtml', ], 'taintedStaticCallWithNoTypes' => [ 'code' => ' 'TaintedHtml', ], 'taintedInstanceCallWithNoTypes' => [ 'code' => 'rawinput();', 'error_message' => 'TaintedHtml', ], 'taintStringObtainedUsingStrval' => [ 'code' => ' 'TaintedHtml', ], 'taintStringObtainedUsingSprintf' => [ 'code' => ' 'TaintedHtml', ], 'encapsulatedString' => [ 'code' => ' 'TaintedHtml', ], 'encapsulatedToStringMagic' => [ 'code' => ' 'TaintedHtml', ], 'castToStringMagic' => [ 'code' => ' 'TaintedHtml', ], 'castToStringViaArgument' => [ 'code' => ' 'TaintedHtml', ], 'toStringTaintInSubclass' => [ 'code' => ' 'TaintedHtml', ], 'implicitToStringMagic' => [ 'code' => ' 'TaintedHtml', ], 'namespacedFunction' => [ 'code' => ' 'TaintedHtml', ], 'print' => [ 'code' => ' 'TaintedHtml - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:27 - Detected tainted HTML in path: $_GET -> $_GET[\'name\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:27) -> call to print (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:27) -> print#1', ], 'printf' => [ 'code' => ' 'TaintedHtml - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:28 - Detected tainted HTML in path: $_GET -> $_GET[\'name\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:28) -> call to printf (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:28) -> printf#1', ], 'print_r' => [ 'code' => ' 'TaintedHtml - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:29 - Detected tainted HTML in path: $_GET -> $_GET[\'name\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:29) -> call to print_r (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:29) -> print_r#1', ], 'var_dump' => [ 'code' => ' 'TaintedHtml - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:30 - Detected tainted HTML in path: $_GET -> $_GET[\'name\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:30) -> call to var_dump (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:30) -> var_dump#1', ], 'var_export' => [ 'code' => ' 'TaintedHtml - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:32 - Detected tainted HTML in path: $_GET -> $_GET[\'name\'] (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:32) -> call to var_export (src' . DIRECTORY_SEPARATOR . 'somefile.php:2:32) -> var_export#1', ], 'unpackArgs' => [ 'code' => ' 'TaintedHtml', ], 'foreachArg' => [ 'code' => ' 'TaintedHtml', ], 'magicPropertyType' => [ 'code' => 'params[$a]; } public function __set(string $a, $value) { $this->params[$a] = $value; } } $m = new Magic(); $m->taint = $_GET["input"]; echo $m->taint;', 'error_message' => 'TaintedHtml', ], 'taintNestedArrayWithOffsetAccessedInForeach' => [ 'code' => ' $_GET["name"], "b" => "foo"]; foreach ($a as $m) { echo $m["a"]; }', 'error_message' => 'TaintedHtml', ], 'taintNestedArrayWithOffsetAccessedExplicitly' => [ 'code' => ' $_GET["name"], "b" => "foo"]; echo $a[0]["a"];', 'error_message' => 'TaintedHtml', ], 'taintThroughArrayMapExplicitClosure' => [ 'code' => ' 'TaintedHtml', ], 'taintThroughArrayMapExplicitTypedClosure' => [ 'code' => ' 'TaintedHtml', ], 'taintThroughArrayMapExplicitArrowFunction' => [ 'code' => ' trim($str), $_GET); echo $get["test"];', 'error_message' => 'TaintedHtml', ], 'taintThroughArrayMapImplicitFunctionCall' => [ 'code' => ' $_GET["name"]]; $get = array_map("trim", $a); echo $get["test"];', 'error_message' => 'TaintedHtml', ], 'taintFilterVarCallback' => [ 'code' => ' "trim"]); echo $get["test"];', 'error_message' => 'TaintedHtml', ], 'taintAfterReconciledType' => [ 'code' => ' 'TaintedHtml', ], 'taintExit' => [ 'code' => ' 'TaintedHtml', ], 'taintSpecializedMethod' => [ 'code' => 'isUnsafe();', 'error_message' => 'TaintedHtml', ], 'taintSpecializedMethodForAnonymousInstance' => [ 'code' => 'isUnsafe();', 'error_message' => 'TaintedHtml', ], 'taintSpecializedMethodForStubMadeInstance' => [ 'code' => 'isUnsafe();', 'error_message' => 'TaintedHtml', ], 'doTaintSpecializedInstanceProperty' => [ 'code' => 'x = $x; } } $b = new StringHolder($_GET["x"]); echo $b->x;', 'error_message' => 'TaintedHtml', ], 'taintUnserialize' => [ 'code' => ' 'TaintedUnserialize', ], 'taintCreateFunction' => [ 'code' => ' 'TaintedEval', ], 'taintException' => [ 'code' => ' 'TaintedHtml', ], 'taintError' => [ 'code' => 'getTraceAsString()}\n"; }', 'error_message' => 'TaintedHtml', ], 'taintThrowable' => [ 'code' => ' 'TaintedHtml', ], 'taintReturnedArray' => [ 'code' => ' 'TaintedHtml', ], 'taintFlow' => [ 'code' => ' return */ function some_stub(string $r): string {} $r = $_GET["untrusted"]; echo some_stub($r);', 'error_message' => 'TaintedHtml', ], 'taintFlowProxy' => [ 'code' => ' 'TaintedCallable', ], 'taintFlowProxyAndReturn' => [ 'code' => ' return */ function some_stub(string $r): string {} $r = $_GET["untrusted"]; echo some_stub($r);', 'error_message' => 'TaintedHtml', ], 'taintFlowMethodProxyAndReturn' => [ 'code' => ' return */ function some_stub(string $r): string {} $r = $_GET["untrusted"]; echo some_stub($r);', 'error_message' => 'TaintedHtml', ], 'taintPopen' => [ 'code' => ' 'TaintedShell', ], 'taintProcOpen' => [ 'code' => ' 'TaintedShell', ], 'taintedCurlInit' => [ 'code' => ' 'TaintedSSRF', ], 'taintedCurlSetOpt' => [ 'code' => ' 'TaintedSSRF', ], 'taintThroughChildConstructorWithoutMethodOverride' => [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } class B extends A {} $b = new B($_GET["bar"]); echo $b->getTaint();', 'error_message' => 'TaintedHtml', ], 'taintThroughChildConstructorCallingParentMethod' => [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } class B extends A {} class C extends B {} $c = new C($_GET["bar"]); function foo(B $b) { echo $b->getTaint(); }', 'error_message' => 'TaintedHtml', ], 'taintThroughChildConstructorCallingGrandParentMethod' => [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } class B extends A {} class C extends B {} $c = new C($_GET["bar"]); function foo(A $a) { echo $a->getTaint(); }', 'error_message' => 'TaintedHtml', ], 'taintThroughChildConstructorWhenMethodOverriddenWithParentConstructorCall' => [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } class B extends A { public function __construct($taint) { parent::__construct($taint); } } $b = new B($_GET["bar"]); echo $b->getTaint();', 'error_message' => 'TaintedHtml', ], 'taintedLdapSearch' => [ 'code' => ' 'TaintedLdap', ], 'taintedFile' => [ 'code' => ' 'TaintedFile', ], 'taintedHeader' => [ 'code' => ' 'TaintedHeader', ], 'taintedCookie' => [ 'code' => ' 'TaintedCookie', ], 'variadicTaintPropagation' => [ 'code' => ' return */ function variadic_test(string $format, ...$args) : string { } echo variadic_test(\'\', \'\', $_GET[\'taint\'], \'\');', 'error_message' => 'TaintedHtml', ], 'potentialTaintThroughChildClassSettingProperty' => [ 'code' => 'taint; } } class B extends A { public function __construct(string $taint) { $this->taint = $taint; } } $b = new B($_GET["bar"]); echo $b->getTaint();', 'error_message' => 'TaintedHtml', ], 'immutableClassTrackInputThroughMethod' => [ 'code' => 'taint = $taint; } public function getTaint() : string { return $this->taint; } } $a = new A($_GET["bar"]); echo $a->getTaint();', 'error_message' => 'TaintedHtml', ], 'strTrReturnTypeTaint' => [ 'code' => ' 'TaintedCookie', ], 'conditionallyEscapedTaintPassedFalse' => [ 'code' => ' 'TaintedHtml', ], 'suppressOneCatchAnother' => [ 'code' => ' 'TaintedHtml', ], 'taintSpecializedTwice' => [ 'code' => ' 'TaintedHtml', ], 'conditionallyEscapedTaintsAll' => [ 'code' => ' 'TaintedHtml', ], 'psalmFlowOnInstanceMethod' => [ 'code' => ' return */ public function esc_like($text) {} /** * @param string $query * @return int|bool * * @psalm-taint-sink sql $query */ public function query($query){} } $wdb = new Wdb(); $order = $wdb->esc_like($_GET["order"]); $res = $wdb->query("SELECT blah FROM tablea ORDER BY ". $order. " DESC");', 'error_message' => 'TaintedSql', ], 'psalmFlowOnStaticMethod' => [ 'code' => ' return */ public static function esc_like($text) {} /** * @param string $query * @return int|bool * * @psalm-taint-sink sql $query */ public static function query($query){} } $order = Wdb::esc_like($_GET["order"]); $res = Wdb::query("SELECT blah FROM tablea ORDER BY ". $order. " DESC");', 'error_message' => 'TaintedSql', ], 'keysAreTainted' => [ 'code' => ' $_) { echo $key; } } takesArray([$_GET["bad"] => "good"]);', 'error_message' => 'TaintedHtml', ], 'resultOfPlusIsTaintedOnArrays' => [ 'code' => ' 'TaintedHtml', ], 'taintArrayKeyWithExplicitSink' => [ 'code' => ' "foo"]);', 'error_message' => 'TaintedHtml', ], 'taintThroughReset' => [ 'code' => ' 'TaintedHtml', ], 'shellExecBacktick' => [ 'code' => ' 'TaintedShell', ], /* // TODO: Stubs do not support this type of inference even with $this->message = $message. // Most uses of getMessage() would be with caught exceptions, so this is not representative of real code. 'taintException' => [ 'getMessage();', 'error_message' => 'TaintedHtml', ], */ 'castToArrayPassTaints' => [ 'code' => ' 'TaintedSql', ], 'taintSinkWithComments' => [ 'code' => ' 'TaintedHtml', ], 'taintEscapedInTryMightNotWork' => [ 'code' => ' 'TaintedHtml', ], 'taintArrayWithOffsetUpdated' => [ 'code' => ' [["c" => "hello"]], "b" => [], ]; $foo["b"][] = [ "c" => $_GET["bad"], ]; bar($foo["b"]); } function bar(array $arr): void { foreach ($arr as $s) { echo $s["c"]; } }', 'error_message' => 'TaintedHtml', ], 'checkMemoizedStaticMethodCallTaints' => [ 'code' => ' 'TaintedHtml', ], 'taintedNewCall' => [ 'code' => ' 'TaintedCallable', ], 'urlencode' => [ /** * urlencode() should only prevent html & has_quotes taints * All other taint types should be unaffected. * We arbitrarily chose system() to test this. */ 'code' => ' 'TaintedShell', ], 'assertMysqliOnlyEscapesSqlTaints1' => [ 'code' => 'escape_string($_GET["a"]);', 'error_message' => 'TaintedHtml', ], 'assertMysqliOnlyEscapesSqlTaints2' => [ 'code' => 'real_escape_string($_GET["a"]);', 'error_message' => 'TaintedHtml', ], 'assertMysqliOnlyEscapesSqlTaints3' => [ 'code' => ' 'TaintedHtml', ], 'assertMysqliOnlyEscapesSqlTaints4' => [ 'code' => ' 'TaintedHtml', ], 'assertDb2OnlyEscapesSqlTaints' => [ 'code' => ' 'TaintedHtml', ], 'assertCubridOnlyEscapesSqlTaints' => [ 'code' => ' 'TaintedHtml', ], 'assertSQLiteOnlyEscapesSqlTaints' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints1' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints2' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints3' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints4' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints5' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints6' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints7' => [ 'code' => ' 'TaintedHtml', ], 'assertPGOnlyEscapesSqlTaints8' => [ 'code' => ' 'TaintedHtml', ], ]; } /** * @param list $expectedIssuesTypes * @test * @dataProvider multipleTaintIssuesAreDetectedDataProvider */ public function multipleTaintIssuesAreDetected(string $code, array $expectedIssuesTypes): void { if (strpos($this->getTestName(), 'SKIPPED-') !== false) { $this->markTestSkipped(); } // disables issue exceptions - we need all, not just the first $this->testConfig->throw_exception = false; $filePath = self::$src_dir_path . 'somefile.php'; $this->addFile($filePath, $code); $this->project_analyzer->trackTaintedInputs(); $this->analyzeFile($filePath, new Context(), false); $actualIssueTypes = array_map( fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', IssueBuffer::getIssuesDataForFile($filePath), ); self::assertSame($expectedIssuesTypes, $actualIssueTypes); } /** * @return array}> */ public function multipleTaintIssuesAreDetectedDataProvider(): array { return [ 'taintSinkFlow' => [ 'code' => ' return * @psalm-taint-sink html $value */ function process(string $value): string {} $data = process((string)($_GET["inject"] ?? "")); exec($data); ', 'expectedIssueTypes' => [ 'TaintedHtml{ function process(string $value): string {} }', 'TaintedShell{ exec($data); }', ], ], 'taintSinkCascade' => [ 'code' => ' [ 'TaintedHtml{ echo $value; }', 'TaintedTextWithQuotes{ echo $value; }', 'TaintedShell{ exec($value); }', 'TaintedFile{ file_get_contents($value); }', ], ], 'taintedIncludes' => [ 'code' => ' [ 'TaintedInclude{ require $first; }', 'TaintedInclude{ require $second; }', ], ], ]; } }