Segurança em PHP
Segurança em PHP: prevenir vulnerabilidades PHP no código, nas dependências Composer e no build
Proteger PHP significa trabalhar em três camadas. No código que escreve: valide a entrada, use consultas parametrizadas e o escaping da framework, e nunca desserialize dados não fidedignos. Nos pacotes Composer de que depende: execute composer audit, faça commit do composer.lock e bloqueie versões inseguras. No CI que os instala: registe o que foi realmente instalado e restrinja para onde o build pode ligar-se. A maioria dos guias de segurança em PHP fica pela primeira camada. Este cobre as três.
Atualizado a
- O seu código Injeção, XSS, desserialização, tratamento de ficheiros, sessões Predefinições da framework, revisão de código, PHPStan, Psalm, Semgrep
- As suas dependências Composer Vulnerabilidades conhecidas, pacotes abandonados e maliciosos, desvios do lockfile, código executado na instalação composer audit, políticas do Composer, Dependabot ou Renovate
- O seu build O que o CI instalou e executou de facto, de onde descarregou, a que conseguia aceder Actions fixadas, tokens com privilégios mínimos, política de egress, SBOM em tempo de build
Nesta página: PHP2Shell, as vulnerabilidades PHP que ainda importam, configuração e versões suportadas, análise estática, segurança das dependências Composer, ataques à cadeia de abastecimento do PHP, o build de CI, SBOM e ferramentas por camada.
PHP2Shell: a vulnerabilidade PHP a corrigir já
A 21 de outubro de 2026, o projeto PHP lançou o PHP 8.5.12, 8.4.27, 8.3.36 e 8.2.35 para corrigir duas vulnerabilidades críticas no próprio PHP que podem permitir a execução remota de código sem autenticação. Ambas têm uma pontuação de 9,2 no CVSS v4.0 e, em conjunto, são conhecidas como PHP2Shell:
- CVE-2026-109618: o tratamento de falta de memória pode interromper o PHP enquanto os dados do pedido estão apenas parcialmente inicializados, e a limpeza do pedido pode depois tratar esses dados inválidos como válidos.
- CVE-2026-109619: o estado pertencente a um pedido pode sobreviver depois de a sua memória ter sido libertada, e o pedido seguinte tratado pelo mesmo worker pode utilizá-lo.
O aviso do PHP indica que todas as instalações que servem pedidos web estão em risco, incluindo PHP-FPM, Apache com mod_php, php-cgi, FrankenPHP e LiteSpeed, e considera o mod_php o caso de maior risco. O PHP 8.1 e versões anteriores não recebem novas versões do projeto PHP. Dada a gravidade, a equipa de segurança do PHP publicou ainda assim backports mínimos do código-fonte para o PHP 7.4, 8.0 e 8.1, para quem compila o PHP a partir do código-fonte ou não tem um fornecedor LTS, e algumas distribuições e fornecedores LTS disponibilizam as suas próprias correções. O que fazer:
- Atualize para uma versão corrigida. Na 8.1 ou anterior, passe para um ramo suportado, instale o pacote com o backport do seu fornecedor ou aplique o backport oficial do código-fonte se compilar o PHP por conta própria.
-
Reconstrua todas as imagens de contentor que incluem PHP e volte a implementá-las. Uma tag de imagem antiga continua
a executar o PHP antigo até ser substituída, por isso verifique o que está implementado e não apenas o que está no
repositório:
docker run --rm IMAGE php -v. -
Se não puder atualizar hoje, aplique as mitigações provisórias do aviso de segurança do PHP, como
session.upload_progress.enabled=0. Defina-o nophp.ini, no pool FPM ou numa configuração do servidor aplicada antes de o pedido ser analisado, e reinicie cada worker. Não confie noini_set()nem no.user.inido FPM para isso: o aviso desaconselha ambos. O PHP é claro: estas mitigações reduzem a exposição, mas não tornam uma instalação segura.
Repare no que o composer audit não lhe pode dizer aqui: o PHP2Shell está no runtime, não num pacote
Composer, por isso nunca aparece no composer.lock. O PHP que executa vem da imagem base ou dos pacotes do
sistema operativo, que é a terceira camada deste guia. Para a tabela de versões, a cronologia e um script que verifica
cada tag de imagem, consulte
PHP2Shell: o que corrigir e como encontrar cada PHP vulnerável.
As vulnerabilidades PHP que ainda importam
As classes de vulnerabilidades das aplicações PHP quase não mudaram numa década. O que mudou é que o Laravel, o Symfony e o PHP moderno fecham a maioria por predefinição, por isso os bugs reais concentram-se agora onde o código contorna essas predefinições. A injeção é A05 no OWASP Top 10:2025, e Software Supply Chain Failures entra como novidade em A03, o tema da segunda metade deste guia.
| Vulnerabilidade | Como surge em PHP | A correção | Laravel e Symfony |
|---|---|---|---|
| Injeção de SQL | Dados do pedido concatenados em SQL, incluindo atalhos para consultas raw | Prepared statements do PDO com parâmetros associados | Eloquent e Doctrine associam os parâmetros; whereRaw(), DB::raw() e DQL concatenado não |
| Cross-site scripting (XSS) | Entrada do utilizador impressa em HTML sem escaping | Escaping na saída com htmlspecialchars() ou com o motor de templates |
Blade {{ }} e Twig fazem escaping por predefinição; {!! !!} e |raw não |
| Cross-site request forgery (CSRF) | Um formulário ou endpoint AJAX que altera o estado aceita qualquer pedido que traga o cookie de sessão | Um token por sessão verificado em cada POST, PUT, PATCH e DELETE, mais cookies SameSite |
O Laravel verifica o token nas rotas web (@csrf nos formulários); os Symfony Forms incluem e verificam um por predefinição; as rotas excluídas da verificação ficam sem proteção |
Injeção de código através de eval() |
Dados do pedido, ou um template ou fórmula que um utilizador pode editar, que chegam a eval() |
Não use eval(); mapeie a entrada para um conjunto fixo de operações ou use um parser para as expressões de que precisa |
Nenhuma framework protege as suas próprias chamadas a eval() |
| Injeção de objetos | unserialize() sobre cookies, dados do pedido ou entradas de cache que um atacante consegue escrever |
Use JSON para dados externos; se tiver de desserializar, recuse objetos | Nenhuma framework protege as suas próprias chamadas a unserialize() |
| Inclusão de ficheiros e uploads | Um caminho construído a partir da entrada e passado a include; uploads guardados sob a raiz web com o nome de ficheiro do cliente |
Lista de permissões para os ficheiros incluídos; guarde os uploads fora da raiz web com nomes gerados | store() do Laravel gera os nomes dos ficheiros; valide o tipo e o tamanho no servidor |
| Type juggling | == a comparar tokens ou hashes, em que "0e123" == "0e456" é verdadeiro |
===, hash_equals() para segredos, password_verify() para palavras-passe |
Os helpers da framework comparam de forma segura; as suas próprias comparações dependem de si |
| Sessões e cookies fracos | IDs de sessão legíveis por JavaScript, enviados por HTTP ou mantidos após o início de sessão | Cookies Secure, HttpOnly e SameSite, modo estrito, um novo ID de sessão após o início de sessão | Ambos regeneram o ID de sessão no início de sessão |
| Server-side request forgery (SSRF) | file_get_contents() ou cURL sobre um URL fornecido pelo utilizador, incluindo wrappers como file:// e phar:// |
Lista de permissões de esquemas e anfitriões, bloqueio de endereços internos, allow_url_fopen desativado onde não é usado |
Sem proteção predefinida |
| Injeção de comandos | Entrada passada a exec(), shell_exec(), system() ou proc_open() |
Evite a shell; passe os argumentos como array ou faça o escaping de cada um com escapeshellarg() |
O Symfony Process e a facade Process do Laravel aceitam um array de argumentos |
| Atribuição em massa | User::create($request->all()) permite que um pedido defina is_admin |
Liste os campos preenchíveis ou passe apenas a entrada validada | Os modelos Eloquent estão protegidos por predefinição; $guarded = [] desativa essa proteção |
Injeção de SQL: vulnerável e corrigido
// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);
// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();
// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get(); // fixed
XSS: vulnerável e corrigido
<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>'; // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>'; // fixed ?>
{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p> {{-- vulnerable --}}
<p>{{ $comment }}</p> {{-- fixed --}}
{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p> {# vulnerable #}
<p>{{ comment }}</p> {# fixed #}
Injeção de objetos: vulnerável e corrigido
// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);
// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);
// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);
A desserialização phar, a variante em que uma operação de ficheiro sobre um caminho phar://
desserializava os metadados do arquivo, foi restringida no PHP 8.0: os metadados phar
já não são desserializados ao abrir
um phar, apenas quando o código chama getMetadata(). É mais uma razão para deixar o PHP 7 para trás.
Inclusão de ficheiros: vulnerável e corrigido
// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';
// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;
Configuração e hardening do runtime
Uma aplicação segura pode ainda assim expor informação através do runtime. Estas definições do php.ini
seguem a OWASP
PHP Configuration Cheat Sheet:
| Definição | Valor em produção | Porquê |
|---|---|---|
display_errors |
Off (com log_errors = On) |
Os stack traces mostram caminhos, consultas e por vezes segredos |
expose_php |
Off |
Deixa de anunciar a versão exata do PHP nos cabeçalhos de resposta |
allow_url_include |
Off |
Impede o include de URLs remotos; obsoleto desde o PHP 7.4 |
allow_url_fopen |
Off se a aplicação não obtém URLs com funções de ficheiro |
Reduz o SSRF através de file_get_contents() |
disable_functions |
Por exemplo exec, shell_exec, system, passthru, proc_open, popen |
Retira a shell a um atacante que consegue executar código; liste apenas o que a sua aplicação nunca chama |
open_basedir |
O diretório da aplicação | Limita os caminhos que as funções de ficheiro do PHP podem abrir; uma salvaguarda, não uma fronteira de isolamento |
| Cookies de sessão | session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax ou Strict, session.use_strict_mode = 1 |
Mantém os IDs de sessão fora de HTTP simples e longe de JavaScript, e rejeita IDs que o servidor não emitiu |
file_uploads |
Off se a aplicação não aceita uploads |
Elimina toda uma via de entrada |
Mantenha-se numa versão de PHP suportada
Segundo o php.net, o PHP dá a cada ramo dois anos de suporte ativo e depois dois anos apenas de correções de segurança. O PHP 8.1 chegou ao fim de vida a 31 de dezembro de 2025, e o PHP 8.2 segue-se a 31 de dezembro de 2026. Um ramo em fim de vida não recebe novas versões do projeto PHP, e foi nessa posição que o PHP2Shell deixou os utilizadores da 8.1: a única correção oficial para eles foi um backport do código-fonte.
- PHP 8.1 Fim de vida, terminou a 31 de dez. de 2025
- PHP 8.2 Apenas correções de segurança, até 31 de dez. de 2026
- PHP 8.3 Apenas correções de segurança, até 31 de dez. de 2027
- PHP 8.4 Suporte ativo, até 31 de dez. de 2026
- PHP 8.5 Suporte ativo, até 31 de dez. de 2027
Definições de produção no Laravel e no Symfony
-
Debug desligado.
APP_DEBUG=falseno Laravel,APP_ENV=prodeAPP_DEBUG=0no Symfony. Uma página de debug em produção mostra a configuração e os valores do ambiente. -
Chaves e segredos. Mantenha
APP_KEY(Laravel) eAPP_SECRET(Symfony) fora do repositório e faça a sua rotação se forem expostos. OAPP_PREVIOUS_KEYSdo Laravel permite rodar a chave sem terminar a sessão de todos os utilizadores. -
Proxies de confiança. Atrás de um balanceador de carga, liste os proxies em que confia (o middleware
de trusted proxies do Laravel,
framework.trusted_proxiesno Symfony), para que um cliente não consiga falsificar o seu endereço IP nem o HTTPS com cabeçalhosX-Forwarded-*.
Análise estática para PHP (SAST)
A análise estática lê o código sem o executar. Três ferramentas open source cobrem a maioria das equipas PHP:
-
PHPStan encontra erros de tipos e bugs com níveis de rigor crescentes. Não é um scanner de segurança,
mas muitos bugs de segurança começam como uma confusão de tipos ou um
nullnão verificado. -
Psalm acrescenta análise de
taint: segue os dados desde fontes como
$_GETe$_COOKIEaté sinks comoecho,includee consultas SQL, e reporta problemas comoTaintedSqleTaintedHtml. O Psalm atual executa-a juntamente com a verificação de tipos;--taint-analysisativa-a para uma única execução, e--report=results.sarifgera SARIF para o GitHub code scanning. - Semgrep suporta PHP como linguagem de disponibilidade geral, com regras da comunidade para erros comuns de PHP e análise de fluxo de dados entre funções na versão comercial.
O SAST gera muito ruído em PHP legado por razões estruturais: includes dinâmicos, variáveis variáveis, métodos mágicos
como __get() e __call(), e convenções de framework que fazem passar os dados por containers
e facades que o analisador não consegue seguir. A ferramenta ou adivinha, e reporta falsos positivos, ou desiste, e
deixa escapar fluxos reais.
Numa base de código antiga, a adoção funciona melhor como uma roquete. Registe os resultados de hoje como baseline
(vendor/bin/phpstan analyse --generate-baseline escreve phpstan-baseline.neon; o Psalm tem
--set-baseline), faça falhar o build apenas com resultados novos e suba o nível à medida que reduz a
baseline. As equipas com bases de código PHP antigas dizem-nos que a correção automática por IA funciona bem para
atualizações de dependências e é irregular nos resultados de código, por isso reveja as correções geradas para
resultados SAST com o mesmo cuidado que qualquer outra alteração. SAST e SCA respondem a perguntas diferentes;
consulte SAST vs SCA.
Segurança das dependências Composer
Uma aplicação Laravel ou Symfony instala dezenas de pacotes Composer, a maioria transitivos. O Composer ganhou verdadeiras funcionalidades de segurança nas últimas versões; esta secção reflete o Composer 2.11, a versão atual.
composer audit
O composer audit verifica os pacotes instalados contra os avisos de segurança da
API do Packagist.org, que reúne avisos
de fontes como a GitHub Advisory Database e o FriendsOfPHP/security-advisories. Reporta também pacotes abandonados e,
desde o Composer 2.10, versões de pacotes assinaladas como malware. Consulte a
documentação do Composer para todas as opções.
composer audit # audit what is installed in vendor/
composer audit --locked # audit composer.lock, whatever is in vendor/
composer audit --no-dev # skip require-dev packages
composer audit --direct # direct dependencies only (Composer 2.11)
composer audit --format=json # table, plain, json or summary
composer audit --abandoned=report # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low
Desde o Composer 2.10, o código de saída é 0 quando não há problemas e 1 quando algo falha a
auditoria, por isso funciona como controlo de CI sem necessidade de analisar a saída. Os pacotes abandonados fazem
falhar a auditoria por predefinição.
Os resultados aceites devem ficar no composer.json, com um motivo, e não numa flag de CI de que ninguém se
lembra. O Composer 2.10 substituiu a maior parte da antiga configuração audit por um bloco
policy:
{
"config": {
"policy": {
"advisories": {
"ignore-id": {
"CVE-1234": "Not affected: we never call the vulnerable parser."
}
},
"abandoned": {
"audit": "report"
}
}
}
}
As chaves antigas (audit.ignore, audit.abandoned, audit.block-insecure)
continuam a funcionar como alternativa enquanto a secção policy correspondente não existir, mas estão
obsoletas. Migre todas as definições de avisos de uma só vez: assim que existe policy.advisories, qualquer
chave audit.* relacionada com avisos é ignorada.
Bloquear versões inseguras
Desde o Composer 2.9 (novembro de 2025), composer update e composer require recusam versões
de pacotes que tenham um aviso de segurança. Era a definição audit.block-insecure e agora é
policy.advisories.block, ativa por predefinição. O Composer 2.10 acrescentou o bloqueio de malware a
partir do feed de malware do Packagist, que se aplica também ao composer install. O Composer 2.11
acrescentou uma política cooldown opcional que retém as versões acabadas de lançar durante um período à sua
escolha, como "7 days", porque muitas versões maliciosas são detetadas e removidas em horas ou dias.
Em CI, dois pormenores contam. Primeiro, o bloqueio por avisos aplica-se quando as versões são resolvidas: uma versão
que já está no composer.lock continua a ser instalada pelo composer install depois de um aviso
ser publicado, por isso continua a precisar do composer audit como controlo. Segundo,
--no-blocking desliga todas as políticas para um comando, algo que vale a pena procurar nos seus
pipelines.
Antes do Composer 2.9, a forma habitual de bloquear versões inseguras era o
roave/security-advisories, um pacote que
declara um conflict com cada versão vulnerável conhecida, construído a partir da GitHub Advisory Database
e do FriendsOfPHP. Continua a funcionar e continua a ajudar em versões antigas do Composer; no Composer 2.9 ou
posterior, o bloqueio integrado cobre os mesmos avisos sem o pacote adicional.
Disciplina do lockfile
- Faça commit do
composer.locknas aplicações, para que todos os ambientes instalem as mesmas versões. -
Use
composer installem CI e nas implementações, nuncacomposer update, que resolve novas versões em tempo real. - Instale os builds de produção com
--no-dev, para que as ferramentas de teste nunca sejam entregues. -
Execute
composer validate --strict: falha quando ocomposer.lockestá desatualizado em relação aocomposer.json.
Um lockfile regista o que o Composer resolveu. Consulte o que um lockfile não regista.
Código na instalação: plugins, scripts e ficheiros carregados em autoload
O Composer executa código das suas dependências em mais sítios do que a maioria das equipas espera, o que o torna o equivalente PHP dos scripts de instalação do npm:
-
Os plugins correm dentro do próprio Composer. Desde o Composer 2.2, a definição
allow-pluginstem de listar cada plugin autorizado a correr, e por predefinição não autoriza nenhum. Trate cada nova entrada como uma nova dependência com acesso à shell. -
Os scripts correm apenas a partir do
composer.jsonraiz, não a partir das dependências. Mas muitas vezes arrancam a sua aplicação. -
Os ficheiros em autoload correm sempre que o autoloader é carregado. Numa aplicação Laravel
standard, o
composer installtermina comphp artisan package:discover, que carrega o autoloader, por isso osfilesem autoload de uma dependência são executados dentro do seu job de CI durante a instalação.
Se um job não precisa de plugins nem de scripts, --no-plugins --no-scripts impede que o código das
dependências corra durante a instalação; execute depois, de forma explícita, os passos de que precisa.
Origens de repositórios e dependency confusion
O Composer pode instalar a partir do Packagist.org, de repositórios Composer privados como o Private Packagist, de
repositórios VCS (URLs Git) e de caminhos locais. Misturar origens privadas e públicas é onde vive a
dependency confusion: se o nome do seu pacote privado também existir no
Packagist.org com uma versão superior, qual recebe? No Composer 2,
todos os repositórios são
canónicos por predefinição: quando o Composer encontra um pacote num repositório, não o procura mais abaixo na
lista, e o Packagist.org fica sempre em último. Definir "canonical": false num repositório remove essa
proteção, por isso faça-o apenas de forma intencional. Os repositórios VCS descarregam o que o anfitrião Git servir
para uma tag, por isso fixe-os por commit no lockfile e reveja as suas alterações.
Pacotes abandonados e sem manutenção
Os parques PHP legados carregam pacotes cujos mantenedores seguiram em frente. O Packagist marca um pacote como
abandonado quando o mantenedor o declara, muitas vezes indicando um substituto, e o composer audit falha
com ele por predefinição. No entanto, muitos pacotes sem manutenção nunca são formalmente abandonados. Verifique a data
da última versão de tudo o que trate entrada, autenticação ou criptografia, e planeie substitutos antes de uma
vulnerabilidade o obrigar.
Atualizar com segurança quando há poucos testes
O caso mais difícil é uma grande aplicação legada com uma dependência vulnerável e poucos testes. O que funciona:
-
Atualize o pacote que traz o vulnerável. A maioria dos resultados é transitiva; execute
composer why vendor/packagepara ver a cadeia e atualize primeiro o pacote a montante dentro da sua restrição. -
Atualize uma coisa de cada vez.
composer update vendor/package --with-dependenciesmantém a alteração pequena o suficiente para a rever no diff docomposer.lock. - Teste primeiro os percursos críticos. Início de sessão, checkout, os principais endpoints da API: um punhado de verificações end-to-end apanha a maioria das falhas de atualização.
- Acrescente testes de caracterização à volta do que a atualização toca. Registe o que o código faz hoje, para notar quando a atualização o altera.
- Faça o rollout por fases e acompanhe as taxas de erro antes de chegar a todos os clientes.
O que o composer audit não vê
O lockfile é um plano, não um registo do build. O composer audit lê vendor/ ou
composer.lock, por isso não vê:
- O próprio PHP e as suas extensões, que vêm da imagem base ou dos pacotes do sistema operativo (o PHP2Shell é um exemplo).
- Extensões PECL e bibliotecas de sistema instaladas na imagem Docker.
- Ferramentas que o seu CI descarrega com
curl, actions que executa e binários que um plugin ou script descarrega. - Código malicioso que ainda não tem aviso nem marca de malware.
Os mesmos limites aplicam-se ao npm audit e ao pip-audit nos respetivos ecossistemas.
Ataques à cadeia de abastecimento do PHP
Quatro incidentes mostram onde os ataques à cadeia de abastecimento contra o PHP acertam de facto, e o que teria detetado cada um.
Março de 2021: commits maliciosos no php-src
A 28 de março de 2021, foram enviados dois commits maliciosos para o repositório php-src em nome de dois programadores do núcleo. A equipa do PHP anunciou que deixaria de manter o seu próprio servidor Git e que os repositórios no GitHub passariam a ser os canónicos. A sua atualização concluiu que os commits tinham sido enviados por HTTPS com autenticação por palavra-passe, possivelmente com credenciais de uma base de dados de utilizadores exposta, e não através de um servidor comprometido. O que o teria detetado: nada no seu próprio pipeline. O alvo era a própria linguagem, o que lembra que o runtime PHP também é uma dependência.
Abril de 2021: injeção de comandos no Composer (CVE-2021-29472)
Uma falha na forma como o Composer tratava URLs de repositórios Mercurial permitia a injeção de comandos. O Packagist informou que o Packagist.org e o Private Packagist estavam teoricamente expostos através de URLs de pacotes manipulados, que não acreditava que tivesse sido explorada, e corrigiu-a no Composer 2.0.13 e 1.10.22. O que o teria detetado: manter o próprio Composer atualizado nas máquinas de desenvolvimento e nas imagens de CI. O gestor de pacotes faz parte da sua cadeia de abastecimento.
Maio de 2023: tomada de contas no Packagist
A 1 de maio de 2023, um atacante entrou em quatro contas inativas do Packagist.org com palavras-passe expostas noutros
sítios, que em conjunto controlavam 14 pacotes, e apontou os pacotes para repositórios bifurcados. As bifurcações
tinham uma descrição alterada e nenhum código malicioso.
O Packagist
restaurou-os na manhã seguinte. O que o teria detetado: um composer.lock no repositório,
já que o composer install usa as origens registadas no lockfile, e a revisão dos diffs do lockfile à
procura de URLs inesperados, como o próprio Packagist recomenda. Para os mantenedores: palavras-passe únicas e
autenticação de dois fatores.
2026: tags sequestradas no laravel-lang e no intercom-php
Em 2026, atacantes com acessos roubados publicaram versões maliciosas de pacotes existentes. O Packagist refere
intercom/intercom-php (30 de abril) e os pacotes laravel-lang (22 de maio) na sua
atualização
sobre segurança da cadeia de abastecimento. No caso do laravel-lang, as tags de versão apontavam para commits de
uma bifurcação maliciosa, e a carga, um ficheiro src/helpers.php
carregado
pelo autoloader do Composer, descarregava um ladrão de credenciais para chaves de cloud, chaves SSH e ficheiros
.env. A Aikido contou 233 versões comprometidas. O que o teria detetado: depois de
assinaladas, a política de malware do Composer 2.10 bloqueia essas versões mesmo no composer install;
antes disso, um período de cooldown mantém de fora as versões acabadas de lançar, uma política de egress impede que a
carga chegue ao seu servidor e um registo de cada build mostra que builds instalaram uma versão maliciosa.
Desde então, o Packagist anunciou que rejeita versões estáveis com tags reatribuídas, publica um registo de transparência das alterações de propriedade e de tags, e assinala malware no Packagist.org desde março de 2026.
Proteger o build PHP em CI
O build é onde o seu código, as suas dependências e os seus segredos se encontram, por isso é aí que um pacote malicioso causa mais danos. Um workflow do GitHub Actions para um build de produção:
name: build
on: [push, pull_request]
permissions:
contents: read # least privilege for GITHUB_TOKEN
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: "8.4"
tools: composer:v2
coverage: none
- run: composer validate --strict
- id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('composer.lock') }}
- run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
- run: composer audit --no-dev
-
Token com privilégios mínimos.
permissions: contents: readimpede que um passo comprometido envie código ou publique versões com oGITHUB_TOKEN. Consulte permissões do token do GitHub. - Actions fixadas. Um SHA de commit completo não pode ser movido como uma tag; o comentário mantém a versão legível para o Dependabot ou o Renovate.
-
Coloque em cache os downloads, não o
vendor/. Colocar em cache a cache de downloads do Composer mantém ocomposer installa correr contra o lockfile em cada job. Umvendor/restaurado é aceite tal como está, e as caches podem ser envenenadas entre workflows; consulte cache do GitHub Actions. - Registe o que foi instalado. O lockfile diz o que deveria ser instalado. Um SBOM registado a partir do build diz o que foi, incluindo o runtime PHP, os pacotes do sistema operativo e os downloads fora do Composer. Consulte SBOM em tempo de build.
- Restrinja para onde o build pode ligar-se. Um build com Composer precisa do Packagist, do GitHub e do seu repositório privado. Uma política de egress que permite apenas esses destinos impede que um ladrão de credenciais envie o que encontrou.
Gerar um SBOM para um projeto PHP
A opção open source habitual é o
plugin CycloneDX para Composer. Por
ser um plugin, precisa de uma entrada em allow-plugins:
composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json
Lê o composer.json em conjunto com os pacotes instalados ou o composer.lock, por isso lista
com precisão as suas dependências Composer e nada mais. O runtime PHP, as suas extensões, os pacotes apt ou apk da
imagem base e as ferramentas que o job descarregou não estão lá. Um SBOM em tempo de build regista-os também, porque é
obtido a partir do que o job descarregou e não dos ficheiros do Composer. Para os formatos e o que um SBOM deve
conter, consulte o que é um SBOM.
Ferramentas de segurança PHP por camada
| Camada | Ferramentas | O que cobrem | O que não veem |
|---|---|---|---|
| Código | PHPStan, Psalm, Semgrep | Bugs e erros de tipos, fluxos de dados contaminados, padrões inseguros conhecidos | Dependências vulneráveis, o runtime, o build |
| Dependências | composer audit, políticas do Composer, roave/security-advisories, Dependabot ou Renovate |
Avisos publicados, pacotes abandonados e assinalados, pull requests de atualização | O seu próprio código e tudo o que está fora do composer.lock |
| Imagem | Scanners de contentores | Pacotes do sistema operativo e da linguagem numa imagem construída | O que o build descarregou e não guardou, e as imagens que ninguém analisou |
| SBOM | Plugin CycloneDX para Composer | Pacotes Composer como SBOM CycloneDX | Tudo o que está fora do Composer |
| Build | CRACI | O que cada job do GitHub Actions instalou de facto, a partir do Composer e de qualquer outra origem, monitorizado ao longo do tempo | O seu código PHP (sem SAST); sistemas de CI que não o GitHub Actions |
Mais sobre imagens: análise de contentores. Menos resultados, e melhores, em todas as camadas: como reduzir o ruído de vulnerabilidades.
Segurança em PHP e CRACI
O CRACI é um runner do GitHub Actions que regista o que cada job instala de facto. Altere runs-on para
craci e cada pacote Composer que o job descarrega fica registado com o seu package URL
(pkg:composer/vendor/package), juntamente com os pacotes do gestor de pacotes do sistema operativo, as
imagens de contentor, as origens Git e outros downloads do mesmo job, incluindo pacotes restaurados a partir de caches
de CI. Cada componente do SBOM esteve no build.
O CRACI compara esses componentes com as vulnerabilidades conhecidas e volta a verificar os SBOM do que entregou à medida que são publicados novos avisos, por isso um CVE divulgado no próximo ano continua a levá-lo aos builds que o contêm. As policy gates podem parar um build com base nos resultados, incluindo um build que contém um CVE específico, e a política de egress limita para onde o job se pode ligar.
O CRACI não analisa o seu código PHP: não há SAST, por opção de conceção. Mantenha o PHPStan, o Psalm ou o Semgrep para a camada de código. Veja cada pacote que o seu build PHP instala.
Quando o CRACI não é a resposta
-
Faz deploy por FTP ou em alojamento partilhado sem CI. Não há build para registar. Comece com o
composer audite umcomposer.lockno repositório. - O seu risco está sobretudo no seu próprio código, como numa aplicação legada cheia de SQL raw. SAST, revisão e testes vêm primeiro.
- Faz os builds fora do GitHub Actions, no GitLab CI ou no Bitbucket Pipelines. O CRACI suporta o GitHub Actions hoje; o GitLab CI e o Jenkins estão no roadmap.
Segurança em PHP: perguntas frequentes
Como proteger código PHP?
Trabalhe em três camadas. No código, valide a entrada, use prepared statements, confie no escaping de saída da framework e nunca desserialize dados não fidedignos. Nas dependências, faça commit do composer.lock, execute composer audit em CI e mantenha ativo o bloqueio de versões inseguras do Composer. No build, fixe as actions, limite o token, restrinja o acesso à rede e registe o que o job instalou.
Quais são as vulnerabilidades PHP mais comuns?
Injeção de SQL, cross-site scripting, cross-site request forgery (CSRF), injeção de código através de eval(), injeção de objetos através de unserialize(), inclusão de ficheiros e uploads inseguros, type juggling com ==, definições de sessão fracas, SSRF através de wrappers de URL, injeção de comandos e atribuição em massa. As dependências Composer vulneráveis e as versões de PHP sem correção, como os bugs PHP2Shell, são uma causa de incidentes igualmente comum.
Qual é o melhor scanner de segurança para PHP?
Nenhuma ferramenta cobre sozinha a segurança em PHP. Use um analisador estático como o PHPStan, o Psalm ou o Semgrep para o código, o composer audit para as dependências Composer, um scanner de contentores para a imagem e um registo em tempo de build do que o CI instalou de facto.
Como funciona o composer audit e o que não vê?
Verifica os pacotes instalados, ou o composer.lock com --locked, contra os avisos da API do Packagist.org, e reporta pacotes abandonados e assinalados como malware. Sai com 1 quando algo falha a auditoria. Não vê o próprio PHP, as extensões PECL, os pacotes do sistema operativo na sua imagem nem nada que o CI descarregue fora do Composer.
Como bloquear pacotes Composer inseguros em CI?
Use o Composer 2.9 ou posterior, que por predefinição bloqueia versões com avisos durante update e require (policy.advisories.block). Como uma versão vulnerável já no lockfile continua a ser instalada, execute também composer audit depois de composer install e faça falhar o job com base no código de saída. Nunca use --no-blocking em CI.
O Laravel é seguro por predefinição?
Em grande parte. O Blade faz escaping da saída, o Eloquent associa os parâmetros das consultas, os modelos estão protegidos contra atribuição em massa e o ID de sessão é regenerado no início de sessão. Essas proteções deixam de funcionar quando o código usa {!! !!}, whereRaw() com entrada do utilizador, $guarded = [] ou APP_DEBUG=true em produção, ou quando a APP_KEY é exposta.
Como atualizar dependências numa aplicação PHP legada com poucos testes?
Atualize o pacote a montante que traz o vulnerável, um pacote de cada vez, com composer update vendor/package --with-dependencies. Teste os percursos críticos, acrescente testes de caracterização à volta do que a atualização toca e faça o rollout por fases.
Como gerar um SBOM para um projeto PHP?
O plugin CycloneDX para Composer (cyclonedx/cyclonedx-php-composer) gera um a partir dos seus pacotes Composer com composer CycloneDX:make-sbom. Lista apenas pacotes Composer. Um SBOM em tempo de build regista também o runtime PHP, os pacotes do sistema operativo e outros downloads do build.
O que é o PHP2Shell?
PHP2Shell é o nome dado a CVE-2026-109618 e CVE-2026-109619, duas vulnerabilidades críticas no próprio PHP que podem permitir a execução remota de código sem autenticação. Estão corrigidas no PHP 8.5.12, 8.4.27, 8.3.36 e 8.2.35, lançados a 21 de outubro de 2026. Consulte PHP2Shell.
Veja cada pacote que o seu build PHP instala
Execute um workflow do GitHub Actions no CRACI e compare o SBOM registado com o seu composer.lock.
Marcar demo