131% más CVE notificadas al día en 2026 que el año pasado.

Seguridad en PHP

Seguridad en PHP: cómo prevenir vulnerabilidades PHP en su código, sus dependencias de Composer y su build

Proteger PHP exige trabajar en tres capas. En el código que escribe: valide la entrada, use consultas parametrizadas y el escapado del framework, y no deserialice nunca datos que no sean de confianza. En los paquetes de Composer de los que depende: ejecute composer audit, haga commit de composer.lock y bloquee las versiones inseguras. En el CI que los instala: registre lo que realmente se instaló y limite a dónde puede conectarse el build. La mayoría de las guías de seguridad en PHP se quedan en la primera capa. Esta cubre las tres.

Actualizado el

  1. Su código Inyección, XSS, deserialización, manejo de archivos, sesiones Valores por defecto del framework, revisión de código, PHPStan, Psalm, Semgrep
  2. Sus dependencias de Composer Vulnerabilidades conocidas, paquetes abandonados y maliciosos, desviaciones del lockfile, código en tiempo de instalación composer audit, políticas de Composer, Dependabot o Renovate
  3. Su build Lo que el CI instaló y ejecutó de verdad, de dónde lo descargó y a qué pudo acceder Actions fijadas, tokens con privilegios mínimos, política de egress, SBOM en tiempo de build
Las tres capas de la seguridad en PHP y las herramientas que actúan en cada una

En esta página: PHP2Shell, las vulnerabilidades PHP que siguen importando, configuración y versiones con soporte, análisis estático, seguridad de las dependencias de Composer, ataques a la cadena de suministro de PHP, el build de CI, SBOM y herramientas por capa.

PHP2Shell: la vulnerabilidad de PHP que hay que parchear ya

El 21 de octubre de 2026 el proyecto PHP publicó PHP 8.5.12, 8.4.27, 8.3.36 y 8.2.35 para corregir dos vulnerabilidades críticas en el propio PHP que pueden permitir la ejecución remota de código sin autenticación. Ambas tienen una puntuación de 9,2 en CVSS v4.0 y, juntas, se conocen como PHP2Shell:

  • CVE-2026-109618: la gestión de falta de memoria puede interrumpir PHP cuando los datos de la petición solo están inicializados en parte, y la limpieza de la petición puede tratar después esos datos no válidos como válidos.
  • CVE-2026-109619: el estado que pertenece a una petición puede sobrevivir después de liberarse su memoria, y la siguiente petición que atienda el mismo worker puede utilizarlo.

El aviso de PHP indica que toda instalación que atiende peticiones web está en riesgo, incluidas PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP y LiteSpeed, y considera mod_php el caso de mayor riesgo. PHP 8.1 y versiones anteriores no reciben ninguna versión nueva del proyecto PHP. Dada la gravedad, el equipo de seguridad de PHP publicó backports mínimos del código fuente para PHP 7.4, 8.0 y 8.1, para quien compila PHP desde el código fuente o no tiene un proveedor LTS, y algunas distribuciones y proveedores LTS ofrecen sus propias correcciones adaptadas. Qué hacer:

  • Actualice a una versión corregida. En 8.1 o anterior, pase a una rama con soporte, instale el paquete con el backport de su proveedor o aplique el backport oficial del código fuente si compila PHP usted mismo.
  • Reconstruya cada imagen de contenedor que incluya PHP y vuelva a desplegarla. Una etiqueta de imagen antigua sigue ejecutando el PHP antiguo hasta que se sustituye, así que compruebe lo que está desplegado, no solo lo que hay en el repositorio: docker run --rm IMAGE php -v.
  • Si hoy no puede actualizar, aplique las mitigaciones provisionales del aviso de seguridad de PHP, como session.upload_progress.enabled=0. Configúrelo en php.ini, en el pool de FPM o en una configuración del servidor que se aplique antes de analizar la petición, y reinicie cada worker. No confíe para ello en ini_set() ni en el .user.ini de FPM: el aviso desaconseja ambos. PHP deja claro que estas mitigaciones reducen la exposición, pero no hacen segura una instalación.

Tenga en cuenta lo que composer audit no puede decirle aquí: PHP2Shell está en el runtime, no en un paquete de Composer, así que nunca aparece en composer.lock. El PHP que ejecuta procede de su imagen base o de los paquetes de su sistema operativo, que es la tercera capa de esta guía. Para la tabla de versiones, la cronología y un script que revisa cada etiqueta de imagen, consulte PHP2Shell: qué parchear y cómo encontrar cada PHP vulnerable.

Las vulnerabilidades PHP que siguen importando

Las clases de vulnerabilidades de las aplicaciones PHP apenas han cambiado en una década. Lo que ha cambiado es que Laravel, Symfony y el PHP moderno cierran la mayoría por defecto, de modo que los fallos reales se concentran ahora donde el código esquiva esos valores por defecto. La inyección es A05 en el OWASP Top 10:2025, y Software Supply Chain Failures entra como novedad en A03, el tema de la segunda mitad de esta guía.

Vulnerabilidad Cómo aparece en PHP La corrección Laravel y Symfony
Inyección SQL Datos de la petición concatenados en SQL, incluidas las vías de escape a consultas en bruto Sentencias preparadas de PDO con parámetros vinculados Eloquent y Doctrine vinculan los parámetros; whereRaw(), DB::raw() y el DQL concatenado no
Cross-site scripting (XSS) Entrada del usuario impresa en HTML sin escapar Escapar en la salida con htmlspecialchars() o con el motor de plantillas Blade {{ }} y Twig escapan por defecto; {!! !!} y |raw no
Cross-site request forgery (CSRF) Un formulario o endpoint AJAX que cambia el estado acepta cualquier petición que lleve la cookie de sesión Un token por sesión comprobado en cada POST, PUT, PATCH y DELETE, más cookies SameSite Laravel comprueba el token en las rutas web (@csrf en los formularios); Symfony Forms incluye y comprueba uno por defecto; las rutas excluidas de la comprobación quedan sin protección
Inyección de código mediante eval() Datos de la petición, o una plantilla o fórmula que un usuario puede editar, que llegan a eval() No use eval(); asigne la entrada a un conjunto fijo de operaciones o use un parser para las expresiones que necesita Ningún framework protege sus propias llamadas a eval()
Inyección de objetos unserialize() sobre cookies, datos de la petición o entradas de caché que un atacante puede escribir Use JSON para los datos externos; si tiene que deserializar, rechace los objetos Ningún framework protege sus propias llamadas a unserialize()
Inclusión de archivos y subidas Una ruta construida a partir de la entrada y pasada a include; subidas guardadas bajo la raíz web con el nombre de archivo del cliente Lista de permitidos para los archivos incluidos; guarde las subidas fuera de la raíz web con nombres generados store() de Laravel genera los nombres de archivo; valide tipo y tamaño en el servidor
Type juggling == al comparar tokens o hashes, donde "0e123" == "0e456" es verdadero ===, hash_equals() para secretos, password_verify() para contraseñas Los helpers del framework comparan de forma segura; sus propias comparaciones dependen de usted
Sesiones y cookies débiles ID de sesión legibles desde JavaScript, enviados por HTTP o mantenidos tras el inicio de sesión Cookies Secure, HttpOnly y SameSite, modo estricto y un nuevo ID de sesión tras el inicio de sesión Ambos regeneran el ID de sesión al iniciar sesión
Server-side request forgery (SSRF) file_get_contents() o cURL sobre una URL que aporta el usuario, incluidos wrappers como file:// y phar:// Lista de permitidos de esquemas y hosts, bloqueo de direcciones internas y allow_url_fopen desactivado si no se usa Sin protección por defecto
Inyección de comandos Entrada pasada a exec(), shell_exec(), system() o proc_open() Evite la shell; pase los argumentos como array o escape cada uno con escapeshellarg() Symfony Process y la facade Process de Laravel aceptan un array de argumentos
Asignación masiva User::create($request->all()) permite que una petición establezca is_admin Enumere los campos rellenables o pase solo la entrada validada Los modelos de Eloquent están protegidos por defecto; $guarded = [] desactiva esa protección

Inyección SQL: vulnerable y corregido

// 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: vulnerable y corregido

<?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 #}

Inyección de objetos: vulnerable y corregido

// 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]);

La deserialización phar, la variante en la que una operación de archivo sobre una ruta phar:// deserializaba los metadatos del archivo, se acotó en PHP 8.0: los metadatos phar ya no se deserializan al abrir un phar, solo cuando el código llama a getMetadata(). Es un motivo más para dejar atrás PHP 7.

Inclusión de archivos: vulnerable y corregido

// 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;

Configuración y endurecimiento del runtime

Una aplicación segura puede filtrar información a través de su runtime. Estos ajustes de php.ini siguen la OWASP PHP Configuration Cheat Sheet:

Ajuste Valor en producción Por qué
display_errors Off (con log_errors = On) Las trazas muestran rutas, consultas y a veces secretos
expose_php Off Deja de anunciar la versión exacta de PHP en las cabeceras de respuesta
allow_url_include Off Impide el include de URL remotas; obsoleto desde PHP 7.4
allow_url_fopen Off si la aplicación no descarga URL con funciones de archivo Reduce el SSRF a través de file_get_contents()
disable_functions Por ejemplo exec, shell_exec, system, passthru, proc_open, popen Quita la shell a un atacante que logra ejecutar código; incluya solo lo que su aplicación nunca llama
open_basedir El directorio de la aplicación Limita las rutas que pueden abrir las funciones de archivo de PHP; una barrera de protección, no un límite de aislamiento
Cookies de sesión session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax o Strict, session.use_strict_mode = 1 Mantiene los ID de sesión fuera de HTTP sin cifrar y lejos de JavaScript, y rechaza los ID que el servidor no emitió
file_uploads Off si la aplicación no acepta subidas Elimina toda una vía de entrada

Use una versión de PHP con soporte

Según php.net, PHP da a cada rama dos años de soporte activo y después dos años solo de correcciones de seguridad. PHP 8.1 llegó al final de su vida útil el 31 de diciembre de 2025, y PHP 8.2 lo hará el 31 de diciembre de 2026. Una rama al final de su vida útil no recibe versiones nuevas del proyecto PHP, que es la situación en la que PHP2Shell dejó a los usuarios de 8.1: su única corrección oficial fue un backport del código fuente.

  • PHP 8.1 Fin de vida útil, terminó el 31 dic 2025
  • PHP 8.2 Solo correcciones de seguridad, hasta el 31 dic 2026
  • PHP 8.3 Solo correcciones de seguridad, hasta el 31 dic 2027
  • PHP 8.4 Soporte activo, hasta el 31 dic 2026
  • PHP 8.5 Soporte activo, hasta el 31 dic 2027
Versiones de PHP con soporte (fechas de php.net)

Ajustes de producción en Laravel y Symfony

  • Depuración desactivada. APP_DEBUG=false en Laravel, APP_ENV=prod y APP_DEBUG=0 en Symfony. Una página de depuración en producción muestra la configuración y los valores del entorno.
  • Claves y secretos. Mantenga APP_KEY (Laravel) y APP_SECRET (Symfony) fuera del repositorio y rótelos si se filtran. APP_PREVIOUS_KEYS de Laravel le permite rotar la clave sin cerrar la sesión de todos los usuarios.
  • Proxies de confianza. Detrás de un balanceador de carga, enumere los proxies en los que confía (el middleware de proxies de confianza de Laravel, framework.trusted_proxies en Symfony), para que un cliente no pueda falsear su dirección IP ni HTTPS con cabeceras X-Forwarded-*.

Análisis estático para PHP (SAST)

El análisis estático lee su código sin ejecutarlo. Tres herramientas de código abierto cubren a la mayoría de los equipos PHP:

  • PHPStan encuentra errores de tipos y fallos con niveles de rigor crecientes. No es un escáner de seguridad, pero muchos fallos de seguridad empiezan como una confusión de tipos o un null sin comprobar.
  • Psalm añade análisis de taint: sigue los datos desde fuentes como $_GET y $_COOKIE hasta sumideros como echo, include y consultas SQL, e informa de problemas como TaintedSql y TaintedHtml. El Psalm actual lo ejecuta junto con la comprobación de tipos; --taint-analysis lo activa para una sola ejecución, y --report=results.sarif genera SARIF para GitHub code scanning.
  • Semgrep admite PHP como lenguaje de disponibilidad general, con reglas de la comunidad para errores habituales de PHP y análisis de flujo de datos entre funciones en su versión comercial.

El SAST genera mucho ruido en PHP heredado por razones estructurales: includes dinámicos, variables variables, métodos mágicos como __get() y __call(), y convenciones de framework que hacen pasar los datos por contenedores y facades que el analizador no puede seguir. La herramienta o bien adivina, y da falsos positivos, o bien se rinde, y pasa por alto flujos reales.

En una base de código antigua funciona mejor como un trinquete. Registre los hallazgos actuales como línea base (vendor/bin/phpstan analyse --generate-baseline escribe phpstan-baseline.neon; Psalm tiene --set-baseline), haga fallar el build solo con hallazgos nuevos y suba el nivel a medida que reduce la línea base. Los equipos con bases de código PHP longevas nos cuentan que la corrección automática con IA funciona bien para actualizar dependencias y es irregular con los hallazgos de código, así que revise las correcciones generadas para hallazgos SAST con el mismo cuidado que cualquier otro cambio. SAST y SCA responden a preguntas distintas; consulte SAST vs SCA.

Seguridad de las dependencias de Composer

Una aplicación Laravel o Symfony instala decenas de paquetes de Composer, la mayoría transitivos. Composer ha incorporado funciones de seguridad reales en las últimas versiones; esta sección refleja Composer 2.11, la versión actual.

composer audit

composer audit compara los paquetes instalados con los avisos de seguridad de la API de Packagist.org, que reúne avisos de fuentes como la GitHub Advisory Database y FriendsOfPHP/security-advisories. También informa de paquetes abandonados y, desde Composer 2.10, de versiones de paquetes marcadas como malware. Consulte la documentación de Composer para ver todas las opciones.

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 Composer 2.10 el código de salida es 0 cuando no hay problemas y 1 cuando algo no supera la auditoría, así que sirve como control de CI sin necesidad de analizar la salida. Los paquetes abandonados hacen fallar la auditoría por defecto.

Los hallazgos aceptados deben estar en composer.json, con un motivo, y no en un flag de CI que nadie recuerda. Composer 2.10 sustituyó la mayor parte de la antigua configuración audit por un bloque policy:

{
    "config": {
        "policy": {
            "advisories": {
                "ignore-id": {
                    "CVE-1234": "Not affected: we never call the vulnerable parser."
                }
            },
            "abandoned": {
                "audit": "report"
            }
        }
    }
}

Las claves heredadas (audit.ignore, audit.abandoned, audit.block-insecure) siguen funcionando como alternativa mientras falte la sección policy correspondiente, pero están obsoletas. Migre todos los ajustes de avisos a la vez: en cuanto existe policy.advisories, se ignora cualquier clave audit.* relacionada con avisos.

Bloquear versiones inseguras

Desde Composer 2.9 (noviembre de 2025), composer update y composer require rechazan las versiones de paquetes que tienen un aviso de seguridad. Era el ajuste audit.block-insecure y ahora es policy.advisories.block, activado por defecto. Composer 2.10 añadió el bloqueo de malware a partir del feed de malware de Packagist, que también se aplica a composer install. Composer 2.11 añadió una política opcional cooldown que retiene las versiones recién publicadas durante el periodo que usted elija, como "7 days", porque muchas versiones maliciosas se detectan y retiran en horas o días.

En CI importan dos detalles. Primero, el bloqueo por avisos se aplica al resolver versiones: una versión que ya está en composer.lock se sigue instalando con composer install después de publicarse un aviso, así que sigue necesitando composer audit como control. Segundo, --no-blocking desactiva todas las políticas para un comando, algo que conviene buscar en sus pipelines.

Antes de Composer 2.9, la forma habitual de bloquear versiones inseguras era roave/security-advisories, un paquete que declara un conflict con cada versión vulnerable conocida, a partir de la GitHub Advisory Database y de FriendsOfPHP. Sigue funcionando y sigue siendo útil con versiones antiguas de Composer; en Composer 2.9 o posterior, el bloqueo integrado cubre los mismos avisos sin el paquete adicional.

Disciplina con el lockfile

  • Haga commit de composer.lock en las aplicaciones, para que todos los entornos instalen las mismas versiones.
  • Use composer install en CI y en los despliegues, nunca composer update, que resuelve versiones nuevas sobre la marcha.
  • Instale los builds de producción con --no-dev, para que las herramientas de prueba nunca se entreguen.
  • Ejecute composer validate --strict: falla cuando composer.lock está desactualizado respecto a composer.json.

Un lockfile registra lo que Composer resolvió. Consulte lo que un lockfile no registra.

Código en tiempo de instalación: plugins, scripts y archivos autocargados

Composer ejecuta código de sus dependencias en más sitios de los que la mayoría de los equipos espera, lo que lo convierte en el equivalente PHP de los scripts de instalación de npm:

  • Los plugins se ejecutan dentro del propio Composer. Desde Composer 2.2, el ajuste allow-plugins debe enumerar cada plugin autorizado a ejecutarse, y por defecto no autoriza ninguno. Trate cada entrada nueva ahí como una dependencia nueva con acceso a la shell.
  • Los scripts solo se ejecutan desde su composer.json raíz, no desde las dependencias. Pero a menudo arrancan su aplicación.
  • Los archivos autocargados se ejecutan siempre que se carga el autoloader. En una aplicación Laravel estándar, composer install termina con php artisan package:discover, que carga el autoloader, de modo que los files autocargados de una dependencia se ejecutan dentro de su job de CI durante la instalación.

Si un job no necesita plugins ni scripts, --no-plugins --no-scripts evita que se ejecute código de las dependencias durante la instalación; ejecute después de forma explícita los pasos que necesite.

Orígenes de repositorios y dependency confusion

Composer puede instalar desde Packagist.org, repositorios privados de Composer como Private Packagist, repositorios VCS (URL de Git) y rutas locales. Mezclar orígenes privados y públicos es donde aparece la dependency confusion: si el nombre de su paquete privado también existe en Packagist.org con una versión superior, ¿cuál obtiene? En Composer 2, todo repositorio es canónico por defecto: cuando Composer encuentra un paquete en un repositorio, no lo busca más abajo en la lista, y Packagist.org va siempre el último. Configurar "canonical": false en un repositorio elimina esa protección, así que hágalo solo a propósito. Los repositorios VCS descargan lo que el host de Git sirva para una etiqueta, así que fíjelos por commit en el lockfile y revise sus cambios.

Paquetes abandonados y sin mantenimiento

Los parques PHP heredados arrastran paquetes cuyos mantenedores se marcharon. Packagist marca un paquete como abandonado cuando su mantenedor lo indica, a menudo con un sustituto, y composer audit falla por defecto con él. Sin embargo, muchos paquetes sin mantenimiento nunca se abandonan formalmente. Mire la fecha de la última versión de todo lo que maneje entrada, autenticación o criptografía, y planifique los sustitutos antes de que una vulnerabilidad le obligue.

Actualizar con seguridad cuando hay pocas pruebas

El caso más difícil es una gran aplicación heredada con una dependencia vulnerable y pocas pruebas. Lo que funciona:

  1. Actualice el paquete que trae el vulnerable. La mayoría de los hallazgos son transitivos; ejecute composer why vendor/package para ver la cadena y actualice primero el paquete de nivel superior dentro de su restricción.
  2. Actualice una cosa cada vez. composer update vendor/package --with-dependencies mantiene el cambio lo bastante pequeño para revisarlo en el diff de composer.lock.
  3. Pruebe primero los flujos críticos. Inicio de sesión, pago, los principales endpoints de la API: unas pocas pruebas de extremo a extremo detectan la mayoría de las roturas por actualización.
  4. Añada pruebas de caracterización alrededor de lo que toca la actualización. Registre lo que hace el código hoy, para notar cuándo la actualización lo cambia.
  5. Despliegue por fases y vigile las tasas de error antes de que llegue a todos los clientes.

Lo que composer audit no ve

El lockfile es un plan, no un registro del build. composer audit lee vendor/ o composer.lock, así que no ve:

  • El propio PHP y sus extensiones, que proceden de su imagen base o de paquetes del sistema operativo (PHP2Shell es un ejemplo).
  • Extensiones PECL y bibliotecas del sistema instaladas en la imagen Docker.
  • Herramientas que su CI descarga con curl, actions que ejecuta y binarios que descarga un plugin o un script.
  • Código malicioso que todavía no tiene aviso ni marca de malware.

Los mismos límites se aplican a npm audit y a pip-audit en sus ecosistemas.

Ataques a la cadena de suministro de PHP

Cuatro incidentes muestran dónde caen realmente los ataques a la cadena de suministro contra PHP y qué habría detectado cada uno.

Marzo de 2021: commits maliciosos en php-src

El 28 de marzo de 2021 se enviaron dos commits maliciosos al repositorio php-src con los nombres de dos desarrolladores del núcleo. El equipo de PHP anunció que dejaría de operar su propio servidor Git y que los repositorios de GitHub pasarían a ser los canónicos. Su seguimiento concluyó que los commits se habían enviado por HTTPS con autenticación por contraseña, posiblemente con credenciales de una base de datos de usuarios filtrada, y no a través de un servidor comprometido. Qué lo habría detectado: nada en su propio pipeline. El objetivo era el propio lenguaje, un recordatorio de que el runtime de PHP también es una dependencia.

Abril de 2021: inyección de comandos en Composer (CVE-2021-29472)

Un fallo en cómo Composer manejaba las URL de repositorios Mercurial permitía la inyección de comandos. Packagist informó de que Packagist.org y Private Packagist estaban expuestos en teoría mediante URL de paquetes manipuladas, de que no creía que se hubiera explotado, y lo corrigió en Composer 2.0.13 y 1.10.22. Qué lo habría detectado: mantener actualizado el propio Composer en los equipos de desarrollo y en las imágenes de CI. El gestor de paquetes forma parte de su cadena de suministro.

Mayo de 2023: robo de cuentas en Packagist

El 1 de mayo de 2023, un atacante entró en cuatro cuentas inactivas de Packagist.org con contraseñas filtradas en otros sitios, que entre todas controlaban 14 paquetes, y apuntó los paquetes a repositorios bifurcados. Las bifurcaciones llevaban una descripción modificada y ningún código malicioso. Packagist los restauró a la mañana siguiente. Qué lo habría detectado: un composer.lock en el repositorio, ya que composer install usa los orígenes registrados en el lockfile, y revisar los diffs del lockfile en busca de URL inesperadas, como recomienda el propio Packagist. Para los mantenedores: contraseñas únicas y autenticación de dos factores.

2026: etiquetas secuestradas en laravel-lang e intercom-php

En 2026, atacantes con accesos robados publicaron versiones maliciosas de paquetes existentes. Packagist cita intercom/intercom-php (30 de abril) y los paquetes laravel-lang (22 de mayo) en su actualización sobre la seguridad de la cadena de suministro. En el caso de laravel-lang, las etiquetas de versión apuntaban a commits de una bifurcación maliciosa, y la carga útil, un archivo src/helpers.php cargado por el autoloader de Composer, descargaba un ladrón de credenciales dirigido a claves de nube, claves SSH y archivos .env. Aikido contabilizó 233 versiones comprometidas. Qué lo habría detectado: una vez marcadas, la política de malware de Composer 2.10 bloquea esas versiones incluso en composer install; antes de eso, un periodo de cooldown deja fuera las versiones recién publicadas, una política de egress impide que la carga útil llegue a su servidor y un registro de cada build muestra qué builds instalaron una versión maliciosa.

Desde entonces, Packagist ha anunciado que rechaza las versiones estables reetiquetadas, publica un registro de transparencia de los cambios de propiedad y de etiquetas, y marca el malware en Packagist.org desde marzo de 2026.

Proteger el build de PHP en CI

El build es donde se juntan su código, sus dependencias y sus secretos, así que es donde un paquete malicioso hace más daño. Un workflow de GitHub Actions para un build de producción:

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 con privilegios mínimos. permissions: contents: read impide que un paso comprometido envíe código o publique versiones con GITHUB_TOKEN. Consulte permisos del token de GitHub.
  • Actions fijadas. Un SHA de commit completo no se puede mover como una etiqueta; el comentario mantiene la versión legible para Dependabot o Renovate.
  • Guarde en caché las descargas, no vendor/. Guardar en caché la caché de descargas de Composer mantiene composer install ejecutándose contra el lockfile en cada job. Un vendor/ restaurado se da por bueno tal cual, y las cachés pueden envenenarse entre workflows; consulte caché de GitHub Actions.
  • Registre lo que se instaló. El lockfile dice lo que debería instalarse. Un SBOM registrado desde el build dice lo que se instaló, incluidos el runtime de PHP, los paquetes del sistema operativo y las descargas fuera de Composer. Consulte SBOM en tiempo de build.
  • Limite a dónde puede conectarse el build. Un build con Composer necesita Packagist, GitHub y su repositorio privado. Una política de egress que solo permite esos destinos impide que un ladrón de credenciales envíe lo que ha encontrado.

Generar un SBOM para un proyecto PHP

La opción de código abierto habitual es el plugin CycloneDX para Composer. Al ser un plugin, necesita una entrada en allow-plugins:

composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json

Lee composer.json junto con los paquetes instalados o composer.lock, así que enumera con precisión sus dependencias de Composer y nada más. El runtime de PHP, sus extensiones, los paquetes apt o apk de la imagen base y las herramientas que descargó el job no aparecen. Un SBOM en tiempo de build también los registra, porque se obtiene de lo que el job descargó y no de los archivos de Composer. Para los formatos y lo que debe contener un SBOM, consulte qué es un SBOM.

Herramientas de seguridad PHP por capa

Capa Herramientas Qué cubren Qué no ven
Código PHPStan, Psalm, Semgrep Fallos y errores de tipos, flujos de datos contaminados, patrones inseguros conocidos Dependencias vulnerables, el runtime, el build
Dependencias composer audit, políticas de Composer, roave/security-advisories, Dependabot o Renovate Avisos publicados, paquetes abandonados y marcados, pull requests de actualización Su propio código y todo lo que esté fuera de composer.lock
Imagen Escáneres de contenedores Paquetes del sistema operativo y del lenguaje en una imagen construida Lo que el build descargó y no conservó, y las imágenes que nadie escaneó
SBOM Plugin CycloneDX para Composer Paquetes de Composer como SBOM CycloneDX Todo lo que está fuera de Composer
Build CRACI Lo que cada job de GitHub Actions instaló realmente, desde Composer y cualquier otro origen, monitorizado a lo largo del tiempo Su código PHP (sin SAST); sistemas de CI distintos de GitHub Actions

Más sobre imágenes: escaneo de contenedores. Menos hallazgos y mejores en todas las capas: cómo reducir el ruido de vulnerabilidades.

Seguridad en PHP y CRACI

CRACI es un runner de GitHub Actions que registra lo que cada job instala realmente. Cambie runs-on a craci y cada paquete de Composer que descarga el job queda registrado con su package URL (pkg:composer/vendor/package), junto con los paquetes del gestor de paquetes del sistema operativo, las imágenes de contenedor, los orígenes Git y otras descargas del mismo job, incluidos los paquetes restaurados desde cachés de CI. Cada componente del SBOM estuvo en el build.

CRACI compara esos componentes con las vulnerabilidades conocidas y vuelve a comprobar los SBOM de lo que usted entregó a medida que se publican avisos nuevos, de modo que un CVE divulgado el año que viene le sigue llevando a los builds que lo contienen. Las policy gates pueden detener un build por sus hallazgos, incluido un build que contiene un CVE concreto, y la política de egress limita a dónde puede conectarse el job.

CRACI no escanea su código PHP: no hay SAST, por diseño. Mantenga PHPStan, Psalm o Semgrep para la capa de código. Vea cada paquete que instala su build de PHP.

Cuándo CRACI no es la respuesta

  • Despliega por FTP o en un hosting compartido sin CI. No hay build que registrar. Empiece con composer audit y un composer.lock en el repositorio.
  • Su riesgo está sobre todo en su propio código, como en una aplicación heredada llena de SQL en bruto. SAST, revisión y pruebas van primero.
  • Hace los builds fuera de GitHub Actions, en GitLab CI o Bitbucket Pipelines. CRACI admite GitHub Actions hoy; GitLab CI y Jenkins están en la hoja de ruta.

Seguridad en PHP: preguntas frecuentes

¿Cómo protejo el código PHP?

Trabaje en tres capas. En su código, valide la entrada, use sentencias preparadas, apóyese en el escapado de salida del framework y no deserialice nunca datos que no sean de confianza. En sus dependencias, haga commit de composer.lock, ejecute composer audit en CI y mantenga activado el bloqueo de versiones inseguras de Composer. En su build, fije las actions, limite el token, restrinja el acceso a la red y registre lo que instaló el job.

¿Cuáles son las vulnerabilidades PHP más comunes?

Inyección SQL, cross-site scripting, cross-site request forgery (CSRF), inyección de código mediante eval(), inyección de objetos mediante unserialize(), inclusión de archivos y subidas inseguras, type juggling con ==, ajustes de sesión débiles, SSRF mediante wrappers de URL, inyección de comandos y asignación masiva. Las dependencias de Composer vulnerables y las versiones de PHP sin parchear, como los fallos PHP2Shell, causan incidentes con la misma frecuencia.

¿Cuál es el mejor escáner de seguridad para PHP?

Ninguna herramienta cubre por sí sola la seguridad en PHP. Use un analizador estático como PHPStan, Psalm o Semgrep para su código, composer audit para sus dependencias de Composer, un escáner de contenedores para la imagen y un registro en tiempo de build de lo que el CI instaló realmente.

¿Cómo funciona composer audit y qué no ve?

Compara los paquetes instalados, o composer.lock con --locked, con los avisos de la API de Packagist.org, e informa de los paquetes abandonados y de los marcados como malware. Sale con 1 cuando algo no supera la auditoría. No ve el propio PHP, las extensiones PECL, los paquetes del sistema operativo de su imagen ni nada de lo que el CI descarga fuera de Composer.

¿Cómo bloqueo los paquetes de Composer inseguros en CI?

Use Composer 2.9 o posterior, que por defecto bloquea las versiones con avisos durante update y require (policy.advisories.block). Como una versión vulnerable fijada en composer.lock se sigue instalando, ejecute también composer audit después de composer install y haga fallar el job según su código de salida. No use nunca --no-blocking en CI.

¿Laravel es seguro por defecto?

En gran medida. Blade escapa la salida, Eloquent vincula los parámetros de las consultas, los modelos están protegidos contra la asignación masiva y el ID de sesión se regenera al iniciar sesión. Esas protecciones dejan de funcionar cuando el código usa {!! !!}, whereRaw() con entrada del usuario, $guarded = [] o APP_DEBUG=true en producción, o cuando se filtra APP_KEY.

¿Cómo actualizo las dependencias de una aplicación PHP heredada con pocas pruebas?

Actualice el paquete de nivel superior que trae el vulnerable, un paquete cada vez, con composer update vendor/package --with-dependencies. Pruebe los flujos críticos, añada pruebas de caracterización alrededor de lo que toca la actualización y despliegue por fases.

¿Cómo genero un SBOM para un proyecto PHP?

El plugin CycloneDX para Composer (cyclonedx/cyclonedx-php-composer) genera uno a partir de sus paquetes de Composer con composer CycloneDX:make-sbom. Solo enumera paquetes de Composer. Un SBOM en tiempo de build registra además el runtime de PHP, los paquetes del sistema operativo y otras descargas del build.

¿Qué es PHP2Shell?

PHP2Shell es el nombre de CVE-2026-109618 y CVE-2026-109619, dos vulnerabilidades críticas en el propio PHP que pueden permitir la ejecución remota de código sin autenticación. Están corregidas en PHP 8.5.12, 8.4.27, 8.3.36 y 8.2.35, publicadas el 21 de octubre de 2026. Consulte PHP2Shell.

Vea cada paquete que instala su build de PHP

Ejecute un workflow de GitHub Actions en CRACI y compare el SBOM registrado con su composer.lock.

Reservar una demo