Situação atual: PHP2Shell. Duas vulnerabilidades críticas do PHP (CVSS 9,2) permitem executar código malicioso sem autenticação. Patches disponíveis desde 21/10/2026.O que fazer agora →

Evitar Command Injection em PHP

Como os programadores evitam a Command Injection em PHP: substituir comandos de sistema por funções PHP, usar proc_open sem shell, verificar argumentos e encontrar pontos perigosos no código existente.

Atualizado em 6 min de leituraDesenvolvimento seguro

Em resumo: A forma mais segura de evitar a Command Injection em PHP é não chamar qualquer shell: muitas tarefas são resolvidas diretamente por funções ou bibliotecas PHP. Onde é necessário um programa externo, passe o comando, desde o PHP 7.4, como array ao proc_open(); o PHP inicia então o programa sem shell. Verifique os valores contra uma lista de permissão, termine as opções com -- e deixe o processo correr com permissões mínimas.

Porque é que uma chamada ao sistema tem tanto poder

Tratamento de imagem, geração de PDF, arquivo, análise antivírus, consulta de uma entrada DNS: para tarefas como estas, as aplicações PHP chamam de bom grado programas externos. Tecnicamente é cómodo, basta uma linha com exec() ou shell_exec(). Do ponto de vista da segurança, é um dos pontos mais perigosos do código.

Se um comando é montado como texto e executado por uma shell, é a shell que decide o que é comando e o que são dados. Se um valor externo chegar aí sem verificação, pode alterar o significado do comando. No pior dos casos, a consequência é a mesma do PHP2Shell: execução de comandos arbitrários no servidor com as permissões do processo PHP.

A diferença: o PHP2Shell corrige-se com uma atualização do PHP. Uma Command Injection no código próprio persiste após qualquer atualização. A isso acresce que tais pontos estão muitas vezes em scripts auxiliares pensados apenas para administradores, por exemplo para manutenção, diagnóstico ou importações, e que mais tarde continuam acessíveis sem proteção.

Onde nasce a Command Injection em PHP

Está em risco qualquer ponto em que o PHP inicia uma shell ou um programa. As funções para isso são:

Função Usa uma shell? Nota
exec(), system(), passthru(), shell_exec() sim comando como texto, a shell interpreta-o
operador de acento grave sim equivale a shell_exec()
popen() sim comando como texto
proc_open() com string sim comando como texto
proc_open() com array não desde o PHP 7.4, os argumentos são passados diretamente
mail() com quinto parâmetro indiretamente parâmetros adicionais para o programa de correio

Um padrão tipicamente inseguro tem este aspeto. O nome do ficheiro vem do pedido e vai parar diretamente à linha de comando:

<?php
// INSEGURO: o valor do pedido passa a fazer parte de um comando de shell
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);

Primeira escolha: nenhuma shell

A proteção mais eficaz é substituir a chamada ao sistema. A OWASP indica-o expressamente como primeira linha de defesa. Para muitas tarefas existem no PHP funções ou extensões próprias:

  • Diretórios e ficheiros: mkdir(), copy(), rename(), unlink()
  • Arquivos: ZipArchive ou PharData
  • Imagens: GD ou Imagick como extensão PHP
  • Consultas de rede: dns_get_record(), gethostbyname(), cURL
  • Somas de verificação: hash_file()

Assim desaparece por completo a questão de saber que carateres uma shell interpreta. Além disso, as funções para comandos de sistema podem então ser bloqueadas na configuração, como descreve o guia Usar corretamente o PHP disable_functions.

Quando é necessário um programa externo: proc_open com array

Desde o PHP 7.4, o proc_open() aceita o comando como array. Segundo o manual do PHP, o processo é então iniciado diretamente, sem passar pela shell, e o PHP assume o necessário mascaramento dos argumentos. Carateres como ponto e vírgula ou pipe deixam de ter significado especial.

<?php
declare(strict_types=1);

$name = basename((string) ($_POST['datei'] ?? ''));
if (!preg_match('/^[a-z0-9_-]{1,64}\.(jpg|png)$/', $name)) {
    throw new InvalidArgumentException('Ungültiger Dateiname.');
}

$befehl = ['/usr/bin/convert', '/srv/app/uploads/' . $name, '-resize', '800x800',
           '/srv/app/vorschau/' . $name];

$prozess = proc_open($befehl, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($prozess)) {
    throw new RuntimeException('Programm konnte nicht gestartet werden.');
}
$fehlerausgabe = stream_get_contents($pipes[2]);
$status = proc_close($prozess);

Três coisas tornam o exemplo robusto: o programa está fixo no código com o caminho completo, o nome do ficheiro é verificado contra um padrão estreito e cada argumento variável começa por um caminho absoluto, pelo que não pode ser lido como opção. Onde um valor está sozinho, termine as opções com --, desde que o programa o suporte segundo a documentação, ou rejeite valores que comecem por um hífen.

Em tarefas que precisam regularmente de programas externos, por exemplo tratamento de imagens ou PDF em maior escala, vale a pena desacoplar: a aplicação web limita-se a colocar um trabalho numa fila de espera, e um serviço de fundo separado, com utilizador e permissões próprios, executa-o. O processo PHP que responde a pedidos vindos da Internet deixa então de precisar de funções para comandos de sistema, e estas podem ser-lhe bloqueadas. Os programas externos devem ainda ter limites de tempo e de memória, para que um único trabalho não bloqueie o servidor.

Um caso especial é o mail(). O quinto parâmetro passa opções adicionais ao programa de correio do sistema. Valores de formulários, por exemplo um endereço de remetente, nunca podem aí chegar sem verificação. Mais robusta é uma biblioteca de correio que envie por SMTP e não precise de chamada ao sistema.

escapeshellarg: aplicar corretamente e conhecer os limites

Onde o código existente ainda trabalha com linhas de comando, o escapeshellarg() para cada valor individual é a medida mínima. A função coloca o valor entre aspas, de modo que a shell o trate como exatamente um argumento. O escapeshellcmd() é inadequado para entradas do utilizador, porque permite parâmetros adicionais, como a OWASP mostra com um exemplo.

Limites importantes do escapeshellarg():

  1. A injeção de argumentos continua possível: um valor mascarado que comece por um hífen é, mesmo assim, lido pelo programa de destino como opção.
  2. O Windows comporta-se de forma diferente: aí a função substitui, segundo o manual, certos carateres como % e " por espaços; os valores chegam, portanto, alterados.
  3. Conjunto de carateres: consoante a definição de locale do servidor, os carateres não ASCII podem perder-se.

Encontrar os pontos de ocorrência no código existente

Uma pesquisa de chamadas ao sistema faz-se depressa e dá uma lista fácil de gerir:

grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'

Cada ocorrência precisa de resposta a três perguntas: a chamada pode ser substituída por funções PHP? Entram valores do exterior? Com que permissões corre o processo? A última pergunta é decisiva para o dano em caso grave. Um processo PHP com utilizador próprio e sem permissão de escrita sobre o código faz menos estragos, ver Least Privilege: permissões do utilizador do servidor web.

Importante

Se um processo PHP-FPM ou do servidor web iniciar uma shell, é um forte sinal de alerta de intrusão. Como o reconhecer é descrito em Sinal de alerta: o servidor web inicia uma shell.

Como ajuda concretamente a PHP SOS?

  • Procuramos todas as chamadas ao sistema na sua aplicação, nos plugins e nos scripts auxiliares.
  • Substituímos chamadas, sempre que possível, por funções PHP e convertemos as restantes em chamadas sem shell.
  • Bloqueamos na configuração do PHP as funções desnecessárias e verificamos as permissões do utilizador PHP.
  • Testamos as alterações e documentamos que programas a aplicação ainda pode chamar.

A verificação de software próprio é oferecida por nós como auditoria de segurança e teste de intrusão.

Perguntas frequentes

O escapeshellarg chega contra a Command Injection?

Impede que um valor seja executado como outro comando. Não impede que o valor seja lido como opção do programa chamado, se começar por um hífen. Por isso, verifique também os valores e, onde o programa o suporta, termine as opções com --.

Qual é a diferença entre escapeshellarg e escapeshellcmd?

O escapeshellarg trata um único valor como exatamente um argumento. O escapeshellcmd apenas mascara metacarateres numa linha de comando inteira e permite parâmetros adicionais. Para entradas do utilizador, o escapeshellarg é a escolha certa; uma chamada sem shell é ainda melhor.

Devo bloquear exec e system com disable_functions?

Se a sua aplicação não precisa delas, sim. Isso limita os danos após uma intrusão. Mas não substitui código seguro, pois as aplicações que precisam de comandos continuam a precisar das funções.

Como reconheço se uma Command Injection foi explorada?

Um forte sinal de alerta são processos PHP ou do servidor web que iniciam uma shell como sh ou bash. A isso juntam-se processos desconhecidos, novos ficheiros em pastas de upload e ligações de saída invulgares.

Fontes

Contacto imediato

Cada hora conta. Escreva-nos agora.

Descreva brevemente o sistema, a versão do PHP e o que observou. Entramos em contacto consigo, esclarecemos a situação e trabalhamos remotamente até o incidente estar concluído e documentado.

Ajuda imediata