Por qué los gestores de paquetes omiten las versiones parcheadas de los paquetes

Ya están disponibles los parches de php2shell (CVE-2026-109618 y CVE-2026-109619) para PHP en fin de vida útil. Dado que las respuestas del departamento de ventas están tardando más de lo habitual debido a la elevada demanda, la forma más rápida de obtener los paquetes parcheados es comprar ELS para Runtimes en línea (se abre en una pestaña nueva). Lee las preguntas frecuentes sobre los parches

Ir directamente al resumen generado por IA de esta entrada del blog

  • Resume esta entrada con Perplexity
  • Resume esta publicación con Gemini

Principales conclusiones

  • Las versiones modificadas se sitúan por encima de las originales en Maven y Python, pero por debajo de ellas en npm y Gradle.
  • De las ocho comprobaciones de restricciones realizadas en cuatro gestores de paquetes, una versión parcheada solo superó la comprobación en tres ocasiones.
  • La visibilidad del resolver —es decir, si este selecciona una versión parcheada por iniciativa propia— determina si una corrección llega a implementarse.
  • El método que funciona en todos los casos consiste en fijar explícitamente la versión parcheada y volver a compilar la que se distribuye.

He aquí una situación que hemos visto repetirse más de una vez. Un equipo adapta una corrección de seguridad a una biblioteca que no puede actualizar, publica la versión parcheada en su registro interno y sigue adelante. La integración continua (CI) da el visto bueno. Una semana después, el escáner sigue detectando el CVE y, cuando alguien por fin abre el archivo de bloqueo, la versión parcheada no está ahí. No ha fallado nada. El gestor de paquetes simplemente ha analizado la versión corregida, ha decidido que no cumplía los requisitos y, en su lugar, ha instalado la versión vulnerable.

Eso no es un error en npm, o Gradle, o cualquier otra cosa. Cada ecosistema hace exactamente lo que dicen sus reglas de versión. El problema es que esas reglas se escribieron para que los responsables del mantenimiento de los proyectos originales lanzaran versiones, no para que cualquiera lanzara una corrección sobre la versión de otra persona. Distribuir paquetes de código abierto parcheados a través de los distintos ecosistemas es una parte importante de lo que hacemos en SecureChain, y lo más útil que hemos aprendido es que el número de versión que se le asigna a una corrección es tan importante como la propia corrección.

Qué significa la visibilidad del resolutor

La visibilidad del resolutor se refiere a si un gestor de paquetes seleccionará por sí mismo una compilación parcheada, teniendo en cuenta las restricciones de versión que ya ha declarado un proyecto. Una compilación con alta visibilidad del resolutor es seleccionada por los rangos y las fijaciones existentes sin que nadie tenga que editar el manifiesto. Una compilación con baja visibilidad del resolutor permanece en el registro, totalmente disponible y totalmente ignorada, hasta que alguien la incorpora manualmente.

La visibilidad depende de dos factores. El primero es el orden, es decir, si el ecosistema considera que tu versión parcheada es más reciente o más antigua que la versión original en la que se basa. El segundo es la compatibilidad, es decir, si las restricciones comunes —como un rango de caracteres o un pin exacto— consideran que la versión parcheada es aceptable. Estas dos cuestiones reciben respuestas diferentes según el ecosistema, por lo que una estrategia que funciona a la perfección en un lugar puede fracasar discretamente en otro.

Una versión, cuatro veredictos distintos

Para ilustrarlo con un ejemplo concreto, hemos tomado un único paquete hipotético en la versión upstream 4.7.3, además de una versión parcheada de ese mismo lanzamiento, y las sometimos a la lógica de versiones real que utiliza cada ecosistema. Eso significaba que npmpropio de… semver la biblioteca (7.6.0), el comparador de versiones de Maven 3.9.11, Gradle 8.14, que resuelve las dependencias en un repositorio local, y Python’s packaging biblioteca, que es lo que pip se utiliza para evaluar versiones. La versión parcheada se escribía 4.7.3-patched.2 en todas partes excepto en Python, donde era 4.7.3+patched.2, ya que ese es el formato que establece la especificación de Python para esta tarea. Cada ejecución se realizó desde cero, sin ningún archivo de bloqueo que lo impidiera.

A continuación, les hicimos dos preguntas a cada uno. Con un rango de compatibilidad normal establecido (^4.7.0 para npm, [4.7,4.8) para Maven y Gradle, ~=4.7.0 (para Python), ¿qué versión es la mejor? Y con 4.7.3 Si se declara como una versión exacta, ¿cuál de las dos gana entonces?

Ecosistema Clasificación de versiones parcheadas Selección de productos compatibles Selecciones de Exact 4.7.3
npm Debajo de 4.7.3 Upstream 4.7.3 Upstream 4.7.3
Maven Por encima de 4.7.3 Versión parcheada Upstream 4.7.3
Gradle Debajo de 4.7.3 Upstream 4.7.3 Upstream 4.7.3
Python (pip) Por encima de 4.7.3 Versión parcheada Versión parcheada

Tres victorias de ocho, y dos de ellas son de Python. Maven y Gradle leen los mismos archivos POM de los mismos repositorios y, aun así, llegan a conclusiones opuestas sobre cuál es la versión más reciente. Si esto te sorprende, no eres el único. A algunos de los nuestros también les sorprendió.

Por qué npm trata tu corrección de seguridad como una versión beta

npm sigue el sistema de versionado semántico (SemVer), y según SemVer, un guión después del número de parche indica el inicio de una versión preliminar. Las versiones preliminares se ordenan por debajo de la versión oficial a la que preceden, por lo que 4.7.3-patched.2 se considera más antiguo que 4.7.3. En cuanto a npm En este sentido, tu versión reforzada es una versión candidata a lanzamiento de la versión vulnerable.

La coincidencia empeora las cosas. Por defecto, npm La función «range matching» omite por completo las versiones preliminares, por lo que ^4.7.0 y ~4.7.0 nunca seleccionará la versión parcheada. Ni siquiera un rango escrito como ^4.7.3-patched.0, que ha optado explícitamente por recibir las versiones preliminares, sigue prefiriendo el formato sin formato 4.7.3 cuando ambos estén disponibles, porque 4.7.3 es «más reciente».

El problema que realmente afecta a los equipos es más sutil que eso. Supongamos que tu registro interno contiene el archivo parcheado 4.7.3 construcción y una antigua 4.7.2, pero no en sentido ascendente 4.7.3. Uno esperaría que ^4.7.0 para aterrizar en la zona. En nuestra prueba, se resolvió en 4.7.2, la versión más antigua y que sigue siendo vulnerable, porque 4.7.2 era la versión más reciente disponible que no fuera una versión preliminar. La solución estaba ahí mismo y npm pasó por encima.

Así que en npm, hay que nombrar directamente la versión parcheada. Si foo Si se trata de una dependencia directa, eso solo significa indicar la versión exacta en dependencies. Para las copias que se encuentran más abajo en el árbol, npm Las sustituciones son la herramienta, configurada en la raíz package.json de la solicitud:

{
  "overrides": {
    "foo": "4.7.3-patched.2"
  }
}

Eso obliga a todas las copias de foo en cualquier parte del árbol de la compilación parcheada, ya sea transitiva o no, y npm ls foo Te dirá si se ha quedado atascado. Hay algunas cosas que conviene saber. npm solo tiene en cuenta las modificaciones de la configuración en el proyecto raíz, por lo que una biblioteca no puede establecerlas para sus usuarios. Si foo también es una dependencia directa; su entrada en dependencies debe coincidir exactamente con la sustitución (el "$foo" el formulario de referencia mantiene ambos datos sincronizados), o npm no se instala con un EOVERRIDE error. Y la nave de sustitución permanece estacionada en la antigua base. Una vez que las naves que vienen de arriba... 4.7.4, cualquier proyecto que no incluya la sustitución saltará sin problemas a ella a través de un rango de selección, y 4.7.4 no dispondrás de tu adaptación si la corrección nunca fue aceptada en el proyecto original. Por eso consideramos cada lanzamiento de parches del proyecto original como un motivo para volver a compilar la corrección sobre la nueva base, de modo que 4.7.4-patched.1 existe antes de que nadie lo necesite. Yarn’s resolutions y pnpm‘s overrides siguen la misma idea con su propia sintaxis.

Maven y Gradle leen el mismo archivo POM y no coinciden

El comparador de versiones de Maven reconoce una breve lista de calificadores como alpha, beta, rc, y snapshot, y todo lo que no reconozca se clasifica tras la versión básica. Así pues, en Maven, 4.7.3-patched.2 es más reciente que 4.7.3, y un rango como [4.7,4.8) lo elige. Buenas noticias, en su mayoría.

El problema es que casi nadie utiliza rangos en Maven. Una versión simple en un POM es un requisito no vinculante, y Maven resuelve los conflictos en función de la distancia respecto a la raíz del árbol, no según cuál sea la versión más reciente. Así que, si la declaración más cercana dice 4.7.3, obtienes 4.7.3, y punto. El orden de Maven es compatible con las compilaciones parcheadas, pero su resolución rara vez plantea la cuestión del orden en primer lugar. Para incluir la compilación parcheada, hay que declararla en dependencyManagement en el nivel superior, lo que anula cualquier cosa que haya solicitado el árbol transitivo.

Gradle sigue el principio contrario en cuanto al orden. Su regla es que una parte adicional no numérica hace que la versión sea inferior, y patched no es una de las pocas palabras especiales (rc, snapshot, release, sp, y algunos otros) que tienen su propio espacio en la fila. Así que 4.7.3-patched.2 clasifica a continuación 4.7.3. Gradle también resuelve los conflictos seleccionando la versión más alta del gráfico, por lo que cuando una dependencia solicita la compilación parcheada y otra solicita 4.7.3, nuestra versión de prueba se resolvió con la versión original 4.7.3. La versión vulnerable se impuso porque Gradle la consideró más reciente. La corrección es una versión estricta, declarada con strictly, ya sea en la propia dependencia o dentro de una restricción de dependencia, como en version { strictly '4.7.3-patched.2' }. Una restricción simple no sirve en este caso, porque Gradle solo la considera como un mínimo y 4.7.3 Ya tiene prioridad sobre la versión parcheada a ojos de Gradle.

Python lo hace bien, si eliges el separador adecuado

La especificación de versiones de Python, la PEP 440, tiene algo que las demás no tienen: una etiqueta de versión local pensada precisamente para esta situación. Todo lo que aparece después del signo más es una etiqueta local, y 4.7.3+patched.2 tipos anteriores 4.7.3. Mejor aún, un pin público como ==4.7.3 ignora la etiqueta local al realizar la coincidencia, por lo que un proyecto que ya esté fijado a 4.7.3 aceptará 4.7.3+patched.2 y lo prefiero. No es necesario realizar cambios en el manifiesto. Es la única fila de la tabla en la que ambas respuestas indican la versión parcheada. No obstante, los archivos de bloqueo siguen teniendo su importancia. Un archivo de bloqueo o un archivo de requisitos con hash fijo mantiene la versión anterior hasta que se vuelva a generar, y un entorno que ya tenga 4.7.3 Una vez instalado, permanece en su sitio hasta que alguien lo active pip install --upgrade.

La trampa consiste en utilizar una publicación posterior. 4.7.3.post1 es aún más alto, pero ==4.7.3 no coincide con ella, por lo que todos los proyectos vinculados a la versión exacta del desarrollador la omiten sin avisar. Si publicas compilaciones de Python con parches, la etiqueta local es la que llega a los proyectos vinculados. Al utilizar una versión posterior al lanzamiento, consigues que la corrección solo llegue a quienes ya estaban actualizando.

Hay un pequeño inconveniente práctico. PyPI no acepta subidas con etiquetas de versión locales, por lo que las compilaciones parcheadas tienen que alojarse en otro sitio, normalmente en un índice privado. La forma en que se configura ese índice también es importante desde el punto de vista de la seguridad. pip no da prioridad a un índice sobre otro. Simplemente selecciona la mejor coincidencia de todos los lugares en los que se le indica que busque. Por lo tanto, al señalar pip Utilizar un único índice que sirva tanto para tus compilaciones como para los paquetes de origen es más seguro que añadir un índice adicional, lo que da pie a confusiones con las dependencias.

Qué implica esto si distribuyes o utilizas paquetes parcheados

La lección que hemos sacado de todo esto es que no se puede confiar en que ningún resolutor encuentre una solución por sí solo. Python es el que más se acerca, e incluso en ese caso depende de que se elija la sintaxis de la versión correcta. En todos los demás casos, hay que fijar explícitamente la compilación parcheada, ya sea mediante una anulación, un dependencyManagement entrada, o una versión estricta, y todo lo que se distribuya tiene que recompilarse para que el pin esté realmente incluido. Si eres el propietario de la aplicación, eso supone un único cambio en un solo lugar. Si publicas paquetes que utilizan otros equipos y la corrección llega tres niveles más abajo en el árbol, eso implica recompilar también los paquetes situados por encima, en orden, hasta que el artefacto de nivel superior contenga la versión corregida, porque nadie más abajo en la cadena va a añadir tu sobrescritura por ti. Supone más trabajo que publicar un único artefacto, pero es el único enfoque que hemos encontrado que se comporta de la misma manera en los cuatro ecosistemas.

La otra lección es dejar de interpretar el estado de seguridad a partir de la cadena de versión. Un sufijo indica al resolutor dónde se clasifica una compilación. No aporta ninguna información fiable sobre qué CVE corrige realmente esa compilación, y muchos escáneres detectarán 4.7.3-patched.2, asignarlo de nuevo a 4.7.3, y señalar todos los CVE conocidos para esa versión. Un registro legible por máquina de lo que se ha corregido, como un documento VEX que se incluye con la compilación, es lo que permite a un escáner distinguir la diferencia, siempre y cuando el escáner lo lea realmente.

Nada de esto tiene nada de exótico. Simplemente forma parte de la seguridad de la cadena de suministro que se encuentra en el funcionamiento interno de los gestores de paquetes, y es ahí donde muchas soluciones válidas acaban en el olvido. Si mantienes versiones que el desarrollador original ya no soporta —que es precisamente donde entra en juego el servicio «Endless Lifecycle Support» de TuxCare—, merece la pena dedicar una tarde a comprobar qué es lo que realmente instalan tus propios resolutores.

PREGUNTAS FRECUENTES

¿Qué es la visibilidad del resolutor?

La visibilidad del resolutor se refiere a si un gestor de paquetes seleccionará por sí mismo una compilación parcheada, dadas las restricciones de versión que ya ha declarado un proyecto. Depende de cómo el ecosistema clasifique la versión parcheada con respecto a la versión original y de si las restricciones habituales, como los rangos y las referencias exactas, coinciden con ella.

¿Por qué npm no instala una versión parcheada del paquete?

npm trata cualquier versión que termine en un guion, como 4.7.3-patched.2, como prelanzamiento. Los prelanzamientos se clasifican por debajo del lanzamiento correspondiente y quedan excluidos de los rangos normales con el símbolo de intercalación y la tilde, por lo que npm instala, en su lugar, la versión del desarrollador o una versión anterior.

¿Cómo se puede forzar una dependencia transitiva parcheada en npm?

Añade un overrides acceso a la raíz package.json que indique la versión parcheada exacta. npm A continuación, utiliza esa versión para todas las copias del paquete en el árbol de dependencias, incluidas las transitivas. Si el paquete también es una dependencia directa, su versión en dependencies tiene que coincidir con la anulación, o npm termina con un EOVERRIDE error. Las sustituciones en una biblioteca package.json se ignoran cuando esa biblioteca se instala como dependencia.

¿Considera Maven que un calificador de versión personalizado es más reciente que la versión publicada?

Sí. Maven clasifica los calificadores no reconocidos después de la versión básica, por lo que 4.7.3-patched.2 es más reciente que 4.7.3. Sin embargo, Maven suele resolver los conflictos en función de la distancia en el árbol de dependencias, en lugar de por la versión, por lo que aún necesitas dependencyManagement para asegurarte de que está seleccionada la versión parcheada.

¿Por qué Gradle prefiere la versión original a una compilación parcheada?

Gradle interpreta una parte adicional no numérica como -patched.2 como inferior a la versión estándar, y su mecanismo de resolución de conflictos selecciona la versión más alta. Declara la versión parcheada con strictly, ya sea en la dependencia o dentro de una restricción de dependencia, para forzarla.

¿Se instalará «pip install 4.7.3+patched.2» con un pin ==4.7.3?

Sí, siempre y cuando la versión parcheada esté en un índice pip como se puede ver. Según la PEP 440, un pin público ignora las etiquetas de versión locales a la hora de realizar la coincidencia, y una versión local se ordena por encima de la pública, por lo que una instalación nueva selecciona la compilación parcheada. Un entorno que ya tenga 4.7.3 necesidades pip install --upgrade, y primero hay que volver a generar un archivo de bloqueo.

Resumen
Por qué los gestores de paquetes omiten las versiones parcheadas de los paquetes
Nombre del artículo
Por qué los gestores de paquetes omiten las versiones parcheadas de los paquetes
Descripción
Una versión parcheada, cuatro gestores de paquetes y solo 3 de las 8 comprobaciones de restricciones la detectaron. A continuación explico por qué y cómo hacer que la corrección se mantenga.
Autor
Nombre del editor
TuxCare
Logotipo de la editorial

Únase a más de 4.500 profesionales de Linux y el código abierto.

2 veces al mes. Sin spam.

Índice
Obtenga las respuestas de seguridad de código abierto que necesita

Únase a más de 4.500 profesionales de Linux y el código abierto.

2 veces al mes. Sin spam.