Ma partie préférée dans l'analyse de code statique est de proposer des hypothèses sur les erreurs potentielles dans le code, puis de les vérifier.
Exemple d'hypothèse:
strpos .
Mais il est possible que, même sur quelques millions de lignes de code, un tel diagnostic ne "tire" pas, vous ne voulez donc pas passer beaucoup de temps sur des hypothèses infructueuses.
Aujourd'hui, je vais montrer comment effectuer l'analyse statique la plus simple à l'aide de l'utilitaire phpgrep sans écrire de code.
Contexte
Depuis plusieurs mois, je supporte le linter PHP NoVerify (lisez-le dans l'article NoVerify: Linter pour PHP de l'équipe VKontakte ).
De temps en temps, des idées de nouveaux diagnostics apparaissent dans l'équipe. Il peut y avoir beaucoup d'idées, mais je veux tout vérifier, surtout si le contrôle proposé vise à identifier les défauts critiques.
Auparavant, je développais activement la go-critique et la situation était similaire, la seule différence étant que les codes sources étaient analysés en Go, et non en PHP. Quand j'ai découvert l'utilitaire gogrep , mon monde a basculé. Comme son nom l'indique, cet utilitaire a quelque chose en commun avec grep, seule la recherche est effectuée non pas par des expressions régulières, mais par des modèles de syntaxe (j'expliquerai plus tard ce que cela signifie).
Je ne voulais pas vivre sans grep intelligent, alors un soir, j'ai décidé de m'asseoir et d'écrire phpgrep
.
Cas analysé
Pour être amusant, on se plonge immédiatement dans l'application. Nous analyserons un petit ensemble de projets PHP assez connus et de grande taille disponibles sur GitHub.
Notre kit comprenait les projets suivants:
Pour les personnes qui complotent ce que nous complotons, c'est un ensemble très appétissant.
Alors allons-y!
Utilisation de l'affectation comme expression
Si l'affectation est utilisée comme expression, en outre:
- le contexte attend le résultat d'une opération logique (condition logique) et
- le côté droit de l'expression n'a pas d'effets secondaires et est constant,
c'est très probablement une erreur dans le code.
Pour commencer, prenons les constructions suivantes pour le "contexte logique":
- Expression à l'intérieur de "
if ($cond)
". - La condition de l'opérateur ternaire est: "
$cond ? $x : $y
". - Conditions de continuation pour les boucles "
while ($cond)
" et " for ($init; $cond; $post)
".
Sur le côté droit de l'affectation, nous attendons des constantes ou des littéraux.
Pourquoi avons-nous besoin de telles restrictions? Commençons par (1):
Ici, nous voyons 4 modèles, la seule différence entre lesquels est l'expression assignée (RHS). Commençons par le premier.
Le modèle " if ($_ = []) $_
" capture un if
, auquel un tableau vide est affecté à n'importe quelle expression. $_
correspond à n'importe quelle expression ou instruction.
(RHS) | if ($_ = []) $_ | | | if', , {} LHS
Les exemples suivants utilisent des groupes const , str et num plus complexes. Contrairement à $_
ils décrivent les restrictions sur les opérations compatibles.
const
est une constante nommée ou une constante de classe.str
est un littéral de chaîne de tout type.num
est un littéral numérique de tout type.
Ces schémas suffisent pour réaliser plusieurs opérations sur le boîtier.
⎆ moodle / blocks / rss_client / viewfeed.php # L37 :
if ($courseid = SITEID) { $courseid = 0; }
Le deuxième déclencheur dans Moodle était la dépendance ADOdb . Dans la bibliothèque en amont, le problème est toujours présent.
⎆ ADOdb / drivers / adodb-odbtp.inc.php # L741 :

Il y a beaucoup dans ce fragment, mais pour nous seule la première ligne est pertinente. Au lieu de comparer le champ databaseType
, nous effectuons l'affectation et allons toujours à l'intérieur de la condition.
Un autre endroit intéressant où nous voulons effectuer des actions uniquement pour les enregistrements «corrects», mais au lieu de cela, toujours les exécuter et, en outre, marquer tout enregistrement comme correct!
⎆ moodle / question / format / blackboard_six / formatqti.php # L598 :
Liste étendue de modèles pour cette vérification Répétons ce que nous avons appris:
- Les modèles ressemblent au code php qu'ils trouvent.
$_
représente tout. Vous pouvez comparer avec .
dans les expressions régulières.${"<class>"}
fonctionne comme $_
avec une restriction de type d'élément AST.
Il convient également de souligner que tout sauf les variables est mappé littéralement. Cela signifie que le motif " array(1, 2 + 3)
" ne sera satisfait que par le code identique dans sa structure syntaxique (les espaces n'affectent pas). D'un autre côté, le modèle " array($_, $_)
" satisfait tout littéral de tableau à deux éléments.
Comparer une expression à vous-même
Le besoin de comparer quelque chose avec soi est très rare. Il peut s'agir d'une vérification NaN
, mais au moins la moitié du temps, il s'agit d'une erreur de copier / coller.
⎆ Wikia / app / extensions / SemanticDrilldown / inclut / SD_FilterValue.php # L103 :
if ( $fv1->month == $fv1->month ) return 0;
À droite devrait être " $fv2->month
".
Pour exprimer des parties en double dans un modèle, nous utilisons des variables avec des noms autres que " _
". Le mécanisme de répétition dans un modèle est similaire aux backlinks dans les expressions régulières.
Le modèle " $x == $x
" sera exactement ce que trouve l'exemple ci-dessus. Au lieu de " x
", n'importe quel nom peut être utilisé. Il est seulement important que les noms soient identiques. Les variables de modèle qui ont des noms distinctifs n'ont pas besoin d'avoir le même contenu lors de la capture.
L'exemple suivant a été trouvé en utilisant " $x <= $x
".
⎆ Drupal / core / modules / views / tests / src / Unit / ViewsDataTest.php # L166 :
$prev = $base_tables[$base_tables_keys[$i - 1]]; $current = $base_tables[$base_tables_keys[$i]]; $this->assertTrue( $prev['weight'] <= $current['weight'] && $prev['title'] <= $prev['title'],
Sous-expressions en double
Maintenant que nous connaissons les possibilités de sous-expressions répétées, nous pouvons composer de nombreux motifs intéressants.
Un de mes favoris est " $_ ? $x : $x
".
Il s'agit d'un opérateur ternaire avec des branches vraies / fausses identiques.
⎆ joomla-cms / bibliothèques / src / User / UserHelper.php # L522 :
return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
Les deux branches sont dupliquées, ce qui suggère un problème potentiel dans le code. Si nous regardons le code autour, nous pouvons comprendre ce qui aurait dû être à la place. Pour des raisons de lisibilité, j'ai supprimé une partie du code et réduit le nom de la variable $encrypted
à $enc
.
case 'crypt-blowfish': return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); case 'md5-base64': return ($show_encrypt) ? '{MD5}' . $enc : $enc; case 'ssha': return ($show_encrypt) ? '{SSHA}' . $enc : $enc; case 'smd5': return ($show_encrypt) ? '{SMD5}' . $enc : $enc; case 'sha256': return ($show_encrypt) ? '{SHA256}' . $enc : '{SHA256}' . $enc; default: return ($show_encrypt) ? '{MD5}' . $enc : $enc;
Je parierais que le code a besoin du patch suivant:
- ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted;
Priorités des opérations dangereuses en PHP
Une bonne précaution en PHP est l'utilisation de parenthèses de regroupement partout où il est important d'avoir l'ordre correct des calculs.
Dans de nombreux langages de programmation, l'expression " x & mask != 0
" a une signification intuitive. Si mask
décrit un bit, ce code vérifie qu'à x
ce bit n'est pas égal à zéro. Malheureusement, pour PHP, cette expression sera calculée comme suit: " x & (mask != 0)
", ce qui n'est presque pas toujours ce dont vous avez besoin.
WordPress, Joomla et Moodle utilisent SimplePie .
⎆ SimplePie / library / SimplePie / Locator.php # L254
⎆ SimplePie / library / SimplePie / Locator.php # L384
⎆ SimplePie / library / SimplePie / Locator.php # L412
⎆ SimplePie / library / SimplePie / Sanitize.php # L349
⎆ SimplePie / library / SimplePie.php # L1634
$feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0
SIMPLEPIE_FILE_SOURCE_REMOTE
défini comme 1
, donc l'expression sera équivalente à:
$feed->method & (1 === 0) // => $feed->method & false
Exemples de modèles de recherche Poursuivant le sujet des priorités d'opérations inattendues, vous pouvez lire sur l' opérateur ternaire en PHP . Le habr même l'article lui était consacré: L'ordre d'exécution de l'opérateur ternaire .
Est-il possible de trouver de tels endroits avec phpgrep
? La réponse est oui !
phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_'
Les avantages de la validation des expressions régulières
⎆ Wikia / app / maintenance / wikia / updateCentralInterwiki.inc # L95 :
if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; }
Comme conçu par l'auteur du code, nous vérifions la coïncidence de l'URL avec l'une des 3 options. Désolé symbole .
non blindé, ce qui entraînera le fait qu'au lieu de falloutvault.com
nous pouvons obtenir falloutvaultxcom
sur n'importe quel domaine et passer le test.

Ce n'est pas une erreur spécifique à PHP. Dans toute application où la validation est effectuée via des expressions régulières et où un méta-caractère fait partie de la chaîne en cours de vérification, il existe un risque d'oublier l'échappement là où il est nécessaire et d'obtenir une vulnérabilité.
Vous pouvez trouver de tels endroits en exécutant phpgrep
:
phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b'
Nous introduisons le sous-modèle nommé pat
, qui capture n'importe quel littéral de chaîne, puis lui appliquons un filtre à partir de l'expression régulière.
Les filtres peuvent être appliqués à n'importe quelle variable de modèle. En plus des expressions régulières, il existe également des opérateurs structurels =
et !=
. Une liste complète se trouve dans la documentation .
${"*"}
capture un nombre arbitraire d'arguments, nous n'avons donc pas à nous soucier des paramètres facultatifs de la fonction preg_match
.
Clés en double dans le tableau littéral
En PHP, vous ne recevrez aucun avertissement si vous exécutez ce code:
<?php var_dump(['a' => 1, 'a' => 2]);
Nous pouvons trouver de tels tableaux en utilisant phpgrep
:
[${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}]
Ce modèle peut être décrypté comme suit: "un littéral de tableau dans lequel il y a au moins deux clés identiques dans une position arbitraire." Les expressions ${"*"}
nous aident à décrire une "position arbitraire", permettant des éléments 0-N avant, entre et après les clés qui nous intéressent.
⎆ Wikia / app / extensions / wikia / WikiaMiniUpload / WikiaMiniUpload_body.php # L23 :
$script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
Dans ce cas, ce n'est pas une erreur grossière, mais je connais des cas où la duplication de clés dans de grands tableaux (100+ éléments) comportait au moins un comportement inattendu dans lequel l'une des clés chevauchait la valeur de l'autre.
Ceci conclut notre brève excursion avec des exemples. Si vous en voulez plus, à la fin de l'article décrit comment obtenir tous les résultats.
Qu'est-ce que phpgrep?
La plupart des éditeurs et des IDE utilisent la recherche en texte brut pour rechercher le code (s'il ne s'agit pas de rechercher un caractère spécial tel qu'une classe ou une variable) - en d'autres termes, quelque chose comme grep.
Vous entrez " $x
", recherchez " $x
". Des expressions régulières peuvent être disponibles pour vous, alors vous pouvez réellement essayer d'analyser le code PHP avec des habitués. Parfois, cela fonctionne même si vous recherchez quelque chose d'assez spécifique et simple - par exemple, "n'importe quelle variable avec un suffixe". Mais si cette variable avec un suffixe doit faire partie d'une autre expression composée, des difficultés surviennent.
phpgrep est un outil de recherche pratique de code PHP, qui vous permet de rechercher non pas à l'aide de réguliers orientés texte, mais à l'aide de modèles compatibles avec la syntaxe.
La syntaxe signifie que la langue du modèle reflète la langue cible et ne fonctionne pas sur les caractères individuels, comme le font les expressions régulières. Nous ne faisons également aucune différence avant de formater le code, seule sa structure est importante.
Contenu optionnel: démarrage rapideDémarrage rapide
L'installation
Il existe des versions prêtes à l'emploi pour amd64 pour Linux et Windows , mais si vous avez installé Go, une seule commande suffit pour obtenir un nouveau binaire pour votre plate-forme:
go get -v github.com/quasilyte/phpgrep/cmd/phpgrep
Si $GOPATH/bin
est dans le système $PATH
, la commande phpgrep
deviendra immédiatement disponible. Pour vérifier cela, essayez d'exécuter la commande avec le paramètre -help
:
phpgrep -help
Si rien ne se passe, trouvez où Go a installé le binaire et ajoutez-le à la variable d'environnement $PATH
.
Une manière ancienne et fiable de regarder $GOPATH
, même si elle n'est pas définie explicitement:
go env GOPATH
Utiliser
Créez un fichier test hello.php
:
<?php function f(...$xs) {} f(10); f(20); f(30); f($x); f();
Exécutez phpgrep
dessus:
Nous avons trouvé tous les appels à la fonction f
avec un argument, un nombre dont la valeur n'est pas égale à 20.
Comment phpgrep fonctionne
Pour analyser PHP, la bibliothèque github.com/z7zmey/php-parser est utilisée. C'est assez bon, mais certaines des limitations de phpgrep
découlent des fonctionnalités de l'analyseur utilisé. En particulier, de nombreuses difficultés surviennent lorsque vous essayez de travailler normalement avec des supports.
Le principe de phpgrep
est simple:
- AST est construit à partir du modèle d'entrée, les filtres sont démontés;
- pour chaque fichier d'entrée, un arbre AST complet est construit;
- nous faisons le tour de l'AST de chaque fichier, essayant de trouver de tels sous-arbres qui correspondent au modèle;
- pour chaque résultat, une liste de filtres est appliquée;
- tous les résultats qui ont réussi les filtres sont imprimés à l'écran.
Le plus intéressant est de savoir comment exactement les deux nœuds AST sont appariés pour l'égalité. Parfois trivial: un à un et les méta-nœuds peuvent capturer plus d'un élément. Des exemples de méta nœuds sont ${"*"}
et ${"str"}
.
Conclusion
Il serait malhonnête de parler de phpgrep
sans mentionner la recherche structurelle et le remplacement (SSR) de PhpStorm. Ils résolvent des problèmes similaires, et le SSR a ses avantages, par exemple, l'intégration dans l'IDE, et phpgrep
vante d'être un programme autonome, qui est beaucoup plus facile à mettre, par exemple, sur CI.
Entre autres choses, phpgrep
est également une bibliothèque que vous pouvez utiliser dans vos programmes pour faire correspondre le code PHP. Ceci est particulièrement utile pour le linter et la génération de code.
Je serai heureux si cet outil vous est utile. Si cet article vous motive simplement à regarder dans la direction du SSR susmentionné, c'est aussi bien.

Matériel supplémentaire
La liste complète des modèles utilisés pour l'analyse se trouve dans le fichier patterns.txt . À côté de ce fichier, vous pouvez trouver le script phpgrep-lint.sh
, qui simplifie le lancement de phpgrep
avec une liste de modèles.
L'article ne fournit pas une liste complète des réponses, mais vous pouvez reproduire l'expérience en clonant tous les référentiels nommés et en exécutant phpgrep-lint.sh
sur eux.
Vous pouvez vous inspirer des modèles de test, par exemple, des articles PVS studio . J'ai vraiment aimé Logical Expressions: Mistakes Made by Professionals , qui se transforme en quelque chose comme ceci:
# "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b'
Vous pouvez également être intéressé par la présentation de phpgrep: recherche de code sensible à la syntaxe .
L'article utilise des images de gophers qui ont été créées via gopherkon .