Mi parte favorita en el análisis de código estático es presentar hipótesis sobre posibles errores en el código y luego verificarlas.
Ejemplo de hipótesis:
strpos .
Pero existe la posibilidad de que incluso en unos pocos millones de líneas de código, dicho diagnóstico no se "dispare", por lo que no querrá pasar mucho tiempo en hipótesis fallidas.
Hoy mostraré cómo realizar el análisis estático más simple usando la utilidad phpgrep sin escribir código.
Antecedentes
Desde hace varios meses, he estado apoyando el linter NoVerify PHP (lea sobre esto en el artículo NoVerify: Linter for PHP del equipo VKontakte ).
De vez en cuando, las ideas para nuevos diagnósticos aparecen en el equipo. Puede haber muchas ideas, pero quiero verificar todo, especialmente si la verificación propuesta tiene como objetivo identificar defectos críticos.
Anteriormente, estaba desarrollando activamente Go-Critical y la situación era similar, con la única diferencia de que las fuentes se analizaron en Go y no en PHP. Cuando me enteré de la utilidad gogrep , mi mundo dio un vuelco. Como su nombre lo indica, esta utilidad tiene algo en común con grep, solo que la búsqueda no se realiza mediante expresiones regulares, sino mediante patrones de sintaxis (explicaré más adelante qué significa esto).
No quería vivir sin grep inteligente, así que una noche decidí sentarme y escribir phpgrep
.
Caso analizado
Para ser divertido, nos sumergimos de inmediato en la aplicación. Analizaremos un pequeño conjunto de proyectos PHP bastante conocidos y grandes disponibles en GitHub.
Nuestro kit incluye los siguientes proyectos:
Para las personas que están tramando lo que estamos tramando, este es un conjunto muy apetitoso.
¡Entonces vamos!
Usar la asignación como una expresión
Si la asignación se usa como una expresión, además:
- el contexto espera el resultado de una operación lógica (condición lógica) y
- el lado derecho de la expresión no tiene efectos secundarios y es constante,
Es muy probable que sea un error en el código.
Para empezar, tomemos las siguientes construcciones para el "contexto lógico":
- Expresión dentro de "
if ($cond)
". - La condición del operador ternario es: "
$cond ? $x : $y
". - Condiciones de continuación para los bucles "
while ($cond)
" y " for ($init; $cond; $post)
".
En el lado derecho de la tarea, esperamos constantes o literales.
¿Por qué necesitamos tales restricciones? Comencemos con (1):
Aquí vemos 4 patrones, la única diferencia entre los cuales es la expresión asignada (RHS). Comencemos con el primero.
La plantilla " if ($_ = []) $_
" captura el if
, para el que a cualquier expresión se le asigna una matriz vacía. $_
coincide con cualquier expresión o declaración.
(RHS) | if ($_ = []) $_ | | | if', , {} LHS
Los siguientes ejemplos usan grupos const , str y num más complejos. A diferencia de $_
describen restricciones en operaciones compatibles.
const
es una constante con nombre o constante de clase.str
es un literal de cadena de cualquier tipo.num
es un literal numérico de cualquier tipo.
Estos patrones son suficientes para lograr varias operaciones en el caso.
⎆ moodle / blocks / rss_client / viewfeed.php # L37 :
if ($courseid = SITEID) { $courseid = 0; }
El segundo desencadenante en moodle fue la dependencia ADOdb . En la biblioteca aguas arriba, el problema aún está presente.
⎆ ADOdb / drivers / adodb-odbtp.inc.php # L741 :

Hay mucho en este fragmento, pero para nosotros solo la primera línea es relevante. En lugar de comparar el campo databaseType
, realizamos la asignación y siempre vamos dentro de la condición.
Otro lugar interesante donde queremos realizar acciones solo para registros "correctos", pero en su lugar, ¡siempre ejecutarlos y, además, marcar cualquier registro como correcto!
⎆ moodle / question / format / blackboard_six / formatqti.php # L598 :
Lista extendida de plantillas para esta verificación Repitamos lo que aprendimos:
- Las plantillas se parecen al código php que encuentran.
$_
representa cualquier cosa. Puedes comparar con .
en expresiones regulares${"<class>"}
funciona como $_
con una restricción de tipo de elemento AST.
También vale la pena enfatizar que todo, excepto las variables, está mapeado literalmente. Esto significa que el patrón " array(1, 2 + 3)
" será satisfecho solo por el código idéntico en la estructura sintáctica (los espacios no afectan). Por otro lado, el patrón " array($_, $_)
" satisface cualquier literal de matriz de dos elementos.
Comparar una expresión contigo mismo
La necesidad de comparar algo con uno mismo es muy rara. Puede ser una verificación de NaN
, pero al menos la mitad del tiempo es un error de copiar / pegar.
⎆ Wikia / app / extensiones / SemanticDrilldown / incluye / SD_FilterValue.php # L103 :
if ( $fv1->month == $fv1->month ) return 0;
A la derecha debería estar " $fv2->month
".
Para expresar partes duplicadas en una plantilla, utilizamos variables con nombres distintos de " _
". El mecanismo de repetición en un patrón es similar a los vínculos de retroceso en las expresiones regulares.
El patrón " $x == $x
" será justo lo que encuentra el ejemplo anterior. En lugar de " x
", se puede usar cualquier nombre. Solo es importante que los nombres sean idénticos. No se requiere que las variables de plantilla que tienen nombres distinguidos tengan el mismo contenido al capturar.
El siguiente ejemplo se encontró usando " $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'],
Subexpresiones duplicadas
Ahora que conocemos las posibilidades de subexpresiones repetidas, podemos componer muchos patrones interesantes.
Uno de mis favoritos es " $_ ? $x : $x
".
Este es un operador ternario con ramas verdaderas / falsas idénticas.
⎆ joomla-cms / bibliotecas / src / User / UserHelper.php # L522 :
return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
Ambas ramas están duplicadas, lo que sugiere un problema potencial en el código. Si miramos el código, podemos entender lo que debería haber sido en su lugar. En aras de la legibilidad, corté parte del código y reduje el nombre de la variable $encrypted
a $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;
Apuesto a que el código necesita el siguiente parche:
- ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted;
Prioridades de operación peligrosas en PHP
Una buena precaución en PHP es el uso de corchetes de agrupación donde sea importante tener el orden correcto de los cálculos.
En muchos lenguajes de programación, la expresión " x & mask != 0
" tiene un significado intuitivo. Si la mask
describe un bit, entonces este código verifica que en x
este bit no sea igual a cero. Desafortunadamente, para PHP, esta expresión se calculará así: " x & (mask != 0)
", que casi siempre no es lo que necesita.
WordPress, Joomla y moodle usan 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
define como 1
, por lo que la expresión será equivalente a:
$feed->method & (1 === 0) // => $feed->method & false
Plantillas de búsqueda de muestra Continuando con el tema de las prioridades de operación inesperadas, puede leer sobre el operador ternario en PHP . En habr, incluso el artículo estaba dedicado a él: El orden de ejecución del operador ternario .
¿Es posible encontrar esos lugares con phpgrep
? La respuesta es si !
phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_'
Los beneficios de la validación de expresiones regulares
⎆ Wikia / app / maintenance / wikia / updateCentralInterwiki.inc # L95 :
if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; }
Tal como lo concibió el autor del código, verificamos que la URL coincida con una de las 3 opciones. Lo siento, símbolo .
no blindado, lo que conducirá al hecho de que en lugar de falloutvault.com
podemos obtener falloutvaultxcom
en cualquier dominio y pasar la prueba.

Este no es un error específico de PHP. En cualquier aplicación donde la validación se realiza a través de expresiones regulares y un meta carácter es parte de la cadena que se está verificando, existe el riesgo de olvidar el escape donde se necesita y obtener una vulnerabilidad.
Puede encontrar dichos lugares ejecutando phpgrep
:
phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b'
Introducimos el subpattern pat
nombrado, que captura cualquier cadena literal, y luego le aplicamos un filtro de la expresión regular.
Los filtros se pueden aplicar a cualquier variable de plantilla. Además de las expresiones regulares, también hay operadores estructurales =
y !=
. Se puede encontrar una lista completa en la documentación .
${"*"}
captura un número arbitrario de cualquier argumento, por lo que no tenemos que preocuparnos por los parámetros opcionales de la función preg_match
.
Duplicar claves en matriz literal
En PHP, no recibirá ninguna advertencia si ejecuta este código:
<?php var_dump(['a' => 1, 'a' => 2]);
Podemos encontrar tales matrices usando phpgrep
:
[${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}]
Este patrón se puede descifrar de la siguiente manera: "un literal de matriz en el que hay al menos dos claves idénticas en una posición arbitraria". Las expresiones ${"*"}
nos ayudan a describir una "posición arbitraria", permitiendo elementos 0-N antes, entre y después de las claves que nos interesan.
⎆ Wikia / app / extensiones / wikia / WikiaMiniUpload / WikiaMiniUpload_body.php # L23 :
$script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
En este caso, esto no es un gran error, pero sé de casos en los que la duplicación de claves en matrices grandes (más de 100 elementos) conlleva al menos un comportamiento inesperado en el que una de las claves se superpone al valor de la otra.
Esto concluye nuestra breve excursión con ejemplos. Si desea más, al final del artículo describe cómo obtener todos los resultados.
¿Qué es phpgrep?
La mayoría de los editores e IDE utilizan la búsqueda de texto sin formato para buscar el código (si no se trata de buscar un carácter especial, como una clase o variable), en otras palabras, algo así como grep.
Ingresa " $x
", busca " $x
". Las expresiones regulares pueden estar disponibles para usted, entonces puede intentar analizar el código PHP con los habituales. A veces incluso funciona si está buscando algo bastante específico y simple, por ejemplo, "cualquier variable con algún sufijo". Pero si esta variable con un sufijo debe ser parte de otra expresión compuesta, surgen dificultades.
phpgrep es una herramienta para la búsqueda conveniente de código PHP, que le permite buscar no usando regulares orientados a texto, sino usando plantillas compatibles con la sintaxis.
La sintaxis significa que el idioma de la plantilla refleja el idioma de destino y no opera en caracteres individuales, como lo hacen las expresiones regulares. Tampoco hacemos ninguna diferencia antes de formatear el código, solo su estructura es importante.
Contenido opcional: inicio rápidoInicio rápido
Instalación
Hay versiones de lanzamiento listas para amd64 para Linux y Windows , pero si tiene instalado Go, entonces un comando es suficiente para obtener un nuevo binario para su plataforma:
go get -v github.com/quasilyte/phpgrep/cmd/phpgrep
Si $GOPATH/bin
está en el sistema $PATH
, entonces el comando phpgrep
estará disponible de inmediato. Para verificar esto, intente ejecutar el comando con el parámetro -help
:
phpgrep -help
Si no sucede nada, busque dónde Go instaló el binario y agréguelo a la $PATH
entorno $PATH
.
Una forma antigua y confiable de ver $GOPATH
, incluso si no se establece explícitamente:
go env GOPATH
Uso
Cree un archivo de prueba hello.php
:
<?php function f(...$xs) {} f(10); f(20); f(30); f($x); f();
Ejecute phpgrep
en él:
Encontramos todas las llamadas a la función f
con un argumento, un número cuyo valor no es igual a 20.
Cómo funciona phpgrep
Para analizar PHP, se utiliza la biblioteca github.com/z7zmey/php-parser . Es lo suficientemente bueno, pero algunas de las limitaciones de phpgrep
derivan de las características del analizador utilizado. Especialmente surgen muchas dificultades cuando se trata de trabajar normalmente con paréntesis.
El principio de phpgrep
es simple:
- AST se crea a partir de la plantilla de entrada, los filtros se desmontan;
- para cada archivo de entrada, se construye un árbol AST completo;
- vamos alrededor del AST de cada archivo, tratando de encontrar subárboles que coincidan con el patrón;
- para cada resultado se aplica una lista de filtros;
- Todos los resultados que han pasado los filtros se imprimen en la pantalla.
Lo más interesante es cómo coinciden exactamente los dos nodos AST para la igualdad. A veces trivial: uno a uno, y los metanodos pueden capturar más de un elemento. Ejemplos de metanodos son ${"*"}
y ${"str"}
.
Conclusión
Sería deshonesto hablar sobre phpgrep
sin mencionar la búsqueda estructural y el reemplazo (SSR) de PhpStorm. Resuelven problemas similares, y el SSR tiene sus ventajas, por ejemplo, la integración en el IDE, y phpgrep
jacta de que es un programa independiente, que es mucho más fácil de poner, por ejemplo, en CI.
Entre otras cosas, phpgrep
también es una biblioteca que puede usar en sus programas para hacer coincidir el código PHP. Esto es especialmente útil para la generación de linter y código.
Estaré encantado si esta herramienta es útil para usted. Si este artículo solo te motiva a mirar en la dirección de la SSR antes mencionada, también es bueno.

Materiales adicionales
La lista completa de patrones que se utilizó para el análisis se puede encontrar en el archivo patterns.txt . Junto a este archivo, puede encontrar el script phpgrep-lint.sh
, que simplifica el lanzamiento de phpgrep
con una lista de plantillas.
El artículo no proporciona una lista completa de respuestas, pero puede reproducir el experimento clonando todos los repositorios nombrados y ejecutando phpgrep-lint.sh
en ellos.
Puede inspirarse en plantillas de prueba, por ejemplo, en artículos de estudio de PVS . Realmente me gustó Expresiones lógicas: Errores cometidos por profesionales , que se transforma en algo como esto:
# "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b'
También te puede interesar la presentación de phpgrep: búsqueda de código compatible con la sintaxis .
El artículo utiliza imágenes de gophers que se crearon a través de gopherkon .