Minha parte favorita na análise de código estático é apresentar hipóteses sobre erros em potencial no código e depois verificá-las.
Exemplo de hipótese:
strpos .
Mas há uma chance de que, mesmo em alguns milhões de linhas de código, esse diagnóstico não "atire", portanto você não deseja gastar muito tempo em hipóteses sem êxito.
Hoje vou mostrar como executar a análise estática mais simples usando o utilitário phpgrep sem escrever código.
Antecedentes
Há vários meses, apoio o linter NoVerify PHP (leia sobre isso no artigo NoVerify: Linter for PHP da equipe VKontakte ).
De tempos em tempos, idéias para novos diagnósticos aparecem na equipe. Pode haver muitas idéias, mas quero verificar tudo, principalmente se a verificação proposta tiver como objetivo identificar defeitos críticos.
Anteriormente, eu desenvolvia ativamente o go-critical e a situação era semelhante, com a única diferença de que os códigos-fonte foram analisados no Go e não no PHP. Quando descobri o utilitário gogrep , meu mundo virou de cabeça para baixo. Como o nome indica, esse utilitário tem algo em comum com o grep, apenas a pesquisa é realizada não por expressões regulares, mas por padrões de sintaxe (explicarei mais adiante o que isso significa).
Eu não queria viver sem um grep inteligente, então uma noite eu decidi sentar e escrever o phpgrep
.
Caso analisado
Para ser divertido, imediatamente nos envolvemos no aplicativo. Analisaremos um pequeno conjunto de projetos PHP bastante conhecidos e grandes disponíveis no GitHub.
Nosso kit incluiu os seguintes projetos:
Para as pessoas que estão tramando o que estamos tramando, esse é um cenário muito apetitoso.
Então vamos lá!
Usando atribuição como uma expressão
Se a atribuição for usada como expressão, além disso:
- o contexto espera o resultado de uma operação lógica (condição lógica) e
- o lado direito da expressão não tem efeitos colaterais e é constante,
provavelmente é um erro no código.
Para começar, tomemos as seguintes construções para o "contexto lógico":
- Expressão dentro de "
if ($cond)
". - A condição do operador ternário é: "
$cond ? $x : $y
". - Condições de continuação para loops "
while ($cond)
" e " for ($init; $cond; $post)
".
No lado direito da tarefa, esperamos constantes ou literais.
Por que precisamos de tais restrições? Vamos começar com (1):
Aqui vemos 4 padrões, a única diferença entre os quais é a expressão atribuída (RHS). Vamos começar com o primeiro.
O modelo " if ($_ = []) $_
" captura um if
, que possui uma matriz vazia atribuída a qualquer expressão. $_
corresponde a qualquer expressão ou declaração.
(RHS) | if ($_ = []) $_ | | | if', , {} LHS
Os exemplos a seguir usam grupos const , str e num mais complexos. Ao contrário de $_
eles descrevem restrições em operações compatíveis.
const
é uma constante nomeada ou constante de classe.str
é uma string literal de qualquer tipo.num
é um literal numérico de qualquer tipo.
Esses padrões são suficientes para realizar várias operações no caso.
⎆ moodle / blocks / rss_client / viewfeed.php # L37 :
if ($courseid = SITEID) { $courseid = 0; }
O segundo gatilho no moodle foi a dependência do ADOdb . Na biblioteca upstream, o problema ainda está presente.
⎆ ADOdb / drivers / adodb-odbtp.inc.php # L741 :

Há muito nesse fragmento, mas para nós apenas a primeira linha é relevante. Em vez de comparar o campo databaseType
, executamos a atribuição e sempre entramos na condição.
Outro local interessante onde queremos executar ações apenas para registros "corretos", mas, em vez disso, sempre execute-os e, além disso, marque qualquer registro como correto!
⎆ moodle / question / format / blackboard_six / formatqti.php # L598 :
Lista estendida de modelos para esta verificação Vamos repetir o que aprendemos:
- Os modelos se parecem com o código php que encontram.
$_
significa qualquer coisa. Você pode comparar com .
em expressões regulares.${"<class>"}
funciona como $_
com uma restrição de tipo de elemento AST.
Também vale ressaltar que tudo, exceto as variáveis, é mapeado literalmente. Isso significa que o padrão " array(1, 2 + 3)
" será satisfeito apenas pelo código idêntico na estrutura sintática (os espaços não afetam). Por outro lado, o padrão " array($_, $_)
" satisfaz qualquer literal de array de dois elementos.
Comparando uma expressão consigo mesmo
A necessidade de comparar algo consigo mesmo é muito rara. Pode ser uma verificação de NaN
, mas pelo menos metade do tempo é um erro de copiar / colar.
⎆ Wikia / app / extensions / SemanticDrilldown / includes / SD_FilterValue.php # L103 :
if ( $fv1->month == $fv1->month ) return 0;
À direita deve estar " $fv2->month
".
Para expressar partes duplicadas em um modelo, usamos variáveis com nomes diferentes de " _
". O mecanismo de repetição em um padrão é semelhante aos backlinks em expressões regulares.
O padrão " $x == $x
" será exatamente o que o exemplo acima encontra. Em vez de " x
", qualquer nome pode ser usado. É importante apenas que os nomes sejam idênticos. Variáveis de modelo que possuem nomes distintos não precisam ter o mesmo conteúdo ao capturar.
O exemplo a seguir foi encontrado usando " $x <= $x
".
Up Drupal / core / módulos / visualizações / testes / 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'],
Subexpressões duplicadas
Agora que sabemos das possibilidades de repetidas subexpressões, podemos compor muitos padrões interessantes.
Um dos meus favoritos é " $_ ? $x : $x
".
Este é um operador ternário com ramificações verdadeiras / falsas idênticas.
Joomla-cms / libraries / src / User / UserHelper.php # L522 :
return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
Ambas as ramificações são duplicadas, o que sugere um possível problema no código. Se olharmos o código ao redor, podemos entender o que deveria ter sido. Para facilitar a leitura, recortei parte do código e reduzi o nome da variável $encrypted
para $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;
Aposto que o código precisa do seguinte patch:
- ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted;
Prioridades de operações perigosas em PHP
Uma boa precaução no PHP é o uso de colchetes, sempre que for importante ter a ordem correta dos cálculos.
Em muitas linguagens de programação, a expressão " x & mask != 0
" tem um significado intuitivo. Se a mask
descrever um pouco, esse código verificará que em x
esse bit não é igual a zero. Infelizmente, para PHP, essa expressão será calculada da seguinte forma: " x & (mask != 0)
", que quase sempre não é o que você precisa.
WordPress, Joomla e moodle usam o SimplePie .
⎆ SimplePie / library / SimplePie / Locator.php # L254
⎆ SimplePie / library / SimplePie / Locator.php # L384
⎆ SimplePie / library / SimplePie / Locator.php # L412
⎆ SimplePie / biblioteca / SimplePie / Sanitize.php # L349
⎆ SimplePie / library / SimplePie.php # L1634
$feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0
SIMPLEPIE_FILE_SOURCE_REMOTE
definido como 1
, portanto a expressão será equivalente a:
$feed->method & (1 === 0) // => $feed->method & false
Modelos de pesquisa de amostra Continuando o tópico de prioridades inesperadas de operação, você pode ler sobre o operador ternário em PHP . No habr, até o artigo foi dedicado a ele: a ordem de execução do operador ternário .
É possível encontrar esses lugares com o phpgrep
? A resposta é sim !
phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_'
Os benefícios da validação de expressão regular
⎆ Wikia / app / maintenance / wikia / updateCentralInterwiki.inc # L95 :
if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; }
Conforme concebido pelo autor do código, verificamos o URL por coincidência com uma das três opções. Desculpe símbolo .
não blindado, o que levará ao fato de que, em vez de falloutvault.com
, podemos obter falloutvaultxcom
em qualquer domínio e passar no teste.

Este não é um erro específico do PHP. Em qualquer aplicativo em que a validação é realizada por meio de expressões regulares e um meta caractere faz parte da sequência que está sendo verificada, existe o risco de esquecer o escape onde é necessário e obter uma vulnerabilidade.
Você pode encontrar esses lugares executando o phpgrep
:
phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b'
Introduzimos o subpadrão pat
nomeado, que captura qualquer literal de string e, em seguida, aplicamos um filtro a partir da expressão regular.
Os filtros podem ser aplicados a qualquer variável de modelo. Além das expressões regulares, também existem operadores estruturais =
e !=
. Uma lista completa pode ser encontrada na documentação .
${"*"}
captura um número arbitrário de argumentos, portanto, não precisamos nos preocupar com os parâmetros opcionais da função preg_match
.
Chaves duplicadas no literal da matriz
No PHP, você não receberá nenhum aviso se executar este código:
<?php var_dump(['a' => 1, 'a' => 2]);
Podemos encontrar essas matrizes usando o phpgrep
:
[${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}]
Esse padrão pode ser descriptografado da seguinte forma: "uma matriz literal na qual existem pelo menos duas chaves idênticas em uma posição arbitrária". As expressões ${"*"}
nos ajudam a descrever uma "posição arbitrária", permitindo elementos 0-N antes, entre e depois das chaves de seu interesse.
⎆ Wikia / app / extensions / wikia / WikiaMiniUpload / WikiaMiniUpload_body.php # L23 :
$script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
Nesse caso, isso não é um erro grave, mas eu sei de casos em que a duplicação de chaves em matrizes grandes (mais de 100 elementos) carregava pelo menos um comportamento inesperado no qual uma das chaves se sobrepunha ao valor da outra.
Isso conclui nossa breve excursão com exemplos. Se você quiser mais, no final do artigo descreve como obter todos os resultados.
O que é phpgrep?
A maioria dos editores e IDEs usa pesquisa de texto sem formatação para pesquisar o código (se não for uma pesquisa por um caractere especial, como uma classe ou variável) - em outras palavras, algo como grep.
Você digita " $x
", encontra " $x
". Expressões regulares podem estar disponíveis para você, então você pode realmente tentar analisar o código PHP com regulares. Às vezes, até funciona se você estiver procurando por algo bastante específico e simples - por exemplo, "qualquer variável com algum sufixo". Mas se essa variável com sufixo fizer parte de outra expressão composta, surgirão dificuldades.
O phpgrep é uma ferramenta para a pesquisa conveniente de código PHP, que permite pesquisar não usando regulares orientados a texto, mas usando modelos com reconhecimento de sintaxe.
Reconhecer sintaxe significa que o idioma do modelo reflete o idioma de destino e não opera com caracteres individuais, como as expressões regulares. Também não fazemos diferença antes de formatar o código, apenas sua estrutura é importante.
Conteúdo opcional: Início rápidoInício rápido
Instalação
Existem compilações de versão prontas para o amd64 para Linux e Windows , mas se você tiver o Go instalado, um comando será suficiente para obter um binário novo para sua plataforma:
go get -v github.com/quasilyte/phpgrep/cmd/phpgrep
Se $GOPATH/bin
estiver no sistema $PATH
, o comando phpgrep
ficará imediatamente disponível. Para verificar isso, tente executar o comando com o parâmetro -help
:
phpgrep -help
Se nada acontecer, encontre onde o Go instalou o binário e adicione-o à variável de ambiente $PATH
.
Uma maneira antiga e confiável de analisar o $GOPATH
, mesmo que não esteja definido explicitamente:
go env GOPATH
Use
Crie um arquivo hello.php
teste:
<?php function f(...$xs) {} f(10); f(20); f(30); f($x); f();
Execute o phpgrep
nele:
Encontramos todas as chamadas para a função f
com um argumento, um número cujo valor não é igual a 20.
Como o phpgrep funciona
Para analisar o PHP, a biblioteca github.com/z7zmey/php-parser é usada. É bom o suficiente, mas algumas limitações do phpgrep
seguem os recursos do analisador usado. Especialmente surgem muitas dificuldades ao tentar trabalhar normalmente com colchetes.
O princípio do phpgrep
é simples:
- O AST é construído a partir do modelo de entrada, os filtros são desmontados;
- para cada arquivo de entrada, uma árvore AST completa é construída;
- contornamos o AST de cada arquivo, tentando encontrar as subárvores que correspondem ao padrão;
- para cada resultado é aplicada uma lista de filtros;
- todos os resultados que passaram nos filtros são impressos na tela.
O mais interessante é como exatamente os dois nós AST são correspondidos por igualdade. Às vezes trivial: individual e meta-nós podem capturar mais de um elemento. Exemplos de meta-nós são ${"*"}
e ${"str"}
.
Conclusão
Seria desonesto falar sobre o phpgrep
sem mencionar a pesquisa estrutural e a substituição (SSR) do PhpStorm. Eles resolvem problemas semelhantes, e o SSR tem suas vantagens, por exemplo, integração ao IDE, e o phpgrep
orgulha de ser um programa independente, que é muito mais fácil de colocar, por exemplo, no IC.
Entre outras coisas, o phpgrep
também é uma biblioteca que você pode usar em seus programas para combinar o código PHP. Isso é especialmente útil para geração de código e linter.
Ficarei feliz se esta ferramenta for útil para você. Se este artigo apenas o motiva a olhar na direção do SSR acima mencionado, também é bom.

Materiais adicionais
A lista completa de padrões usados para análise pode ser encontrada no arquivo patterns.txt . Ao lado deste arquivo, você encontra o script phpgrep-lint.sh
, que simplifica o lançamento do phpgrep
com uma lista de modelos.
O artigo não fornece uma lista completa de respostas, mas você pode reproduzir o experimento clonando todos os repositórios nomeados e executando o phpgrep-lint.sh
neles.
Você pode se inspirar nos modelos de teste, por exemplo, nos artigos do estúdio PVS . Eu realmente gostei de Expressões Lógicas: Erros Feitos por Profissionais , que se transforma em algo assim:
# "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b'
Você também pode estar interessado na apresentação de phpgrep: pesquisa de código com reconhecimento de sintaxe .
O artigo usa imagens de esquilos que foram criados através do gopherkon .