Mais 131% CVE comunicadas por dia em 2026 do que no ano passado.

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

  1. 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
  2. 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
  3. 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
As três camadas da segurança em PHP e as ferramentas que atuam em cada uma

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 no php.ini, no pool FPM ou numa configuração do servidor aplicada antes de o pedido ser analisado, e reinicie cada worker. Não confie no ini_set() nem no .user.ini do 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
Versões de PHP suportadas (datas do php.net)

Definições de produção no Laravel e no Symfony

  • Debug desligado. APP_DEBUG=false no Laravel, APP_ENV=prod e APP_DEBUG=0 no 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) e APP_SECRET (Symfony) fora do repositório e faça a sua rotação se forem expostos. O APP_PREVIOUS_KEYS do 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_proxies no Symfony), para que um cliente não consiga falsificar o seu endereço IP nem o HTTPS com cabeçalhos X-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 null não verificado.
  • Psalm acrescenta análise de taint: segue os dados desde fontes como $_GET e $_COOKIE até sinks como echo, include e consultas SQL, e reporta problemas como TaintedSql e TaintedHtml. O Psalm atual executa-a juntamente com a verificação de tipos; --taint-analysis ativa-a para uma única execução, e --report=results.sarif gera 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.lock nas aplicações, para que todos os ambientes instalem as mesmas versões.
  • Use composer install em CI e nas implementações, nunca composer 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 o composer.lock está desatualizado em relação ao composer.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-plugins tem 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.json raiz, 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 install termina com php artisan package:discover, que carrega o autoloader, por isso os files em 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:

  1. Atualize o pacote que traz o vulnerável. A maioria dos resultados é transitiva; execute composer why vendor/package para ver a cadeia e atualize primeiro o pacote a montante dentro da sua restrição.
  2. Atualize uma coisa de cada vez. composer update vendor/package --with-dependencies mantém a alteração pequena o suficiente para a rever no diff do composer.lock.
  3. 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.
  4. 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.
  5. 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: read impede que um passo comprometido envie código ou publique versões com o GITHUB_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 o composer install a correr contra o lockfile em cada job. Um vendor/ 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 audit e um composer.lock no 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