Nota técnica

Go no ejecuta scripts al instalar y verifica los módulos contra un registro público. Detrás de un proyecto mediano hay aun así unos 40 dueños.

360 proyectos de Go: el mediano con dependencias no nombró a entre el 79.5 y el 82.8 % de sus dueños. El Cargo.lock de nono, un sandbox, resuelve código de 184 publishers, más que la mediana de npm.

· Actualizada el 8 de octubre de 2026 · 12 min de lectura
Qué cambió, en el registro de correcciones de norte-labs: ecosystems/go, nono-build, tools/firejail, tools/sbom-tool y tools/npm-audit-signatures.

El 17 de septiembre conté quién publica el código que llega con un npm install: 165 publishers en el proyecto mediano, y el 87 % detrás de nada que el desarrollador nombrara. A una cifra así se le puede objetar que el problema es de npm. Hasta julio, npm ejecutaba por defecto el código que los paquetes traen para la instalación, y npm install no compara lo que descarga con un registro público de transparencia. Un ecosistema sin esas dos cosas estaría en otra situación. Go nunca tuvo la primera y resolvió la segunda hace años, así que medí Go de la misma forma. En las mismas semanas audité herramientas de control, y miré qué dicen cuando no cumplen lo que prometen. Los datos y el código están publicados en github.com/nortesoftware/norte-labs.

Go no tiene scripts de instalación. Por diseño, go get y go build no ejecutan nada que el autor de un paquete haya escrito para ese momento. Y con la configuración por defecto, cada módulo público que se descarga por primera vez se compara con la base de checksums de Go, sum.golang.org, un registro público de transparencia. Así una versión es la misma para todos y no puede cambiar sin que se note. Las dos cosas son ciertas.

Tomé como marco todos los repositorios de GitHub en Go con al menos 100 estrellas y un push en los últimos doce meses, sin forks: 11,015 el 23 de septiembre de 2026. El marco de npm lo construí igual, así que las cifras de Go y de npm se pueden poner lado a lado. Ninguna de las dos habla de todos los proyectos. Saqué 400 al azar con una semilla publicada. 35 no tenían go.mod en la raíz y cinco no se resolvieron por razones de los propios proyectos, así que quedaron 360. Leí los go.mod del grafo a través del proxy. No compilé nada.

Go no tiene cuentas de publisher. Un módulo se nombra por una ruta, no por una cuenta, así que el dueño hay que derivarlo, y derivarlo es un juicio. Publiqué dos reglas con su código, y una cifra para cada una: el dueño del repositorio que clona el comando go, y el dueño del prefijo que declara la ruta. Las dos dan casi lo mismo, así que la regla no mueve el resultado. Las reglas llegaron a GitHub junto con los resultados, no antes.

El proyecto mediano de Go declara 9 módulos y resuelve 81. Detrás de ellos hay 40 o 41 dueños, y el proyecto nombra a 7 u 8. En el proyecto mediano con alguna dependencia, entre el 79.5 y el 82.8 % de los dueños no está detrás de nada que el proyecto haya pedido. En npm era el 87 %. Son rangos porque el proyecto no cuenta como dependencia de sí mismo, y los datos que guardé no dicen si su dueño es también dueño de alguna dependencia. No es la misma unidad: en npm es quien publicó cada versión, una cuenta o un repositorio, y en Go el dueño derivado de una ruta. La proporción se compara mejor, porque en los dos casos mide cuánto de lo que llegó no lo nombró el proyecto.

Lo que logran los dos mecanismos de Go es real, y resuelve otro problema. Sin scripts de instalación, mientras se descarga no se ejecuta nada que haya escrito el autor de un paquete. Con la base de checksums, el contenido de una versión fijada no cambia. Ninguno de los dos reduce cuántos terceros pueden poner código en la compilación. Go cambió cómo llega el código, no cuántos lo publican.

Las medianas son las cifras que se pueden citar, no las medias. Agrupé los proyectos por el ecosistema que aparece en su grafo, como Kubernetes o AWS: 18 grupos y un resto que no coincide con ninguno. Los proyectos de un mismo grupo comparten bastante más dueños entre sí que con el resto, y el efecto de diseño del recuento de dueños es 29.5. Los 94 proyectos del grupo Kubernetes cargan una mediana de unos 135 dueños y nombran a uno de cada diez. De los 52 de ese resto, 18 no tienen ninguna dependencia. En una media pesarían sobre todo los proyectos de Kubernetes.

Go sí conserva una forma de ejecutar, al compilar, algo que el proyecto elige. Desde Go 1.21, de agosto de 2023, un go.mod puede pedir con una línea toolchain un compilador más nuevo que el instalado, y con el valor por defecto, GOTOOLCHAIN=auto, el comando go lo descarga y lo ejecuta antes de compilar. Esa línea solo cuenta en el módulo desde el que se ejecuta el comando, o en su go.work. En una dependencia se ignora. El 12.8 % de los 360 proyectos la trae en su propio go.mod [9.7–16.6], y el agrupamiento casi no mueve esa cifra. Lo que se descarga es una versión publicada de Go, por el proxy y verificada contra la base de checksums. Una línea go más nueva que el Go instalado hace lo mismo, y esa no la tabulé. Por esa línea sí llega una dependencia: cuando go get agrega un módulo que pide un Go más nuevo, el comando cambia de compilador para terminar. No sé en cuántos proyectos pasa.

La verificación de esa descarga es donde apareció algo que ya había visto en las auditorías. En abril de 2026, golang/go#79070 registró que el comando go aceptaba como validación una respuesta de la base de checksums que no traía entrada para el módulo. Un proxy malicioso podía fabricar esa respuesta. Afectaba a cualquier módulo que no estuviera ya en go.sum, y el issue lo describió también para el compilador: "a malicious module proxy can serve altered versions of the Go toolchain", un proxy malicioso puede servir versiones alteradas del compilador de Go. Hacía falta un proxy o una base de checksums que no fueran de confianza, y el issue se cerró una semana después, como CVE-2026-42501. Una entrada ausente se leyó como un módulo sin problemas. Ya en 2023, semanas después de salir Go 1.21, el CVE-2023-39320 mostró que la línea toolchain permitía que el comando go ejecutara scripts y binarios desde la raíz del módulo. La medición también se topó con el mecanismo sin buscarlo. Seis de los primeros once repositorios pedían un Go más nuevo que el go1.24.4 de Debian 13, que el comando go habría descargado y ejecutado, y tuve que fijar a mano la versión de Go con la que medí.

Después hice el mismo recuento sobre una herramienta que existe para contener código ajeno. nono es un sandbox, y una razón para ejecutar un npm install dentro de él es que el árbol de npm es demasiado grande para auditarlo. Leí su Cargo.lock en el commit 121bf37 de su rama principal, del 21 de septiembre, con nono en la versión 0.78.0: 548 versiones de 501 crates del registro. Para cada versión pregunté a crates.io quién la publicó. Fueron 158 cuentas y 26 repositorios que publican desde su CI, 184 publishers. El proyecto mediano de npm tiene 165, y la mitad central de los proyectos va de 94 a 259. Sin la única cuenta de automatización ni los 26 repositorios quedan 157, contra 130 en npm. Conté, como en npm, todo lo que el lockfile resuelve, incluidas las dependencias de desarrollo y las de otras plataformas; una compilación en una máquina usa menos. Contando quién puede publicar, que es otra pregunta, crates.io daba el 24 de septiembre 227 cuentas y 51 equipos de GitHub. Para npm no tengo esa cifra. nono es un proyecto, no una mediana. En publishers queda por encima de la mediana de npm y dentro de su mitad central, como un árbol de npm típico. Si es más grande o más chico que el de una herramienta típica de Rust, no lo sé.

La otra parte del trabajo fueron auditorías de herramientas de control, desde sandboxes de instalación hasta generadores de SBOM. La pregunta en todas fue la misma: cuando el control no puede aplicarse a lo que dice cubrir, ¿lo dice, o sigue en silencio? A algunas las ejecuté y a otras solo las leí, código y documentación, y lo digo en cada caso.

En su forma estrecha, lo que encontré tiene que ver con lo ausente. Un control se configura para una ruta que no existe al arrancar. La salta, con razón, porque no hay nada que restringir, y no dice nada. Después la ruta aparece y nada la cubre. En firejail, que elegí a propósito, una regla de bloqueo para una ruta ausente no deja ninguna línea en ningún nivel de detalle, y la ruta queda legible y escribible dentro de la jaula. Lo segundo lo advierte su manual. Lo que falta es el aviso: una regla que no hizo nada y una que funcionó se ven igual. En nono, también elegido, un perfil que da acceso a una ruta ausente pierde ese permiso sin aviso, según el issue que abrí, nono#1796. Desde la línea de comandos, el mismo caso sí avisa. Ahí lo que se pierde es un acceso, así que falla cerrado, no abierto. El de Go es lo mismo sin rutas: una entrada ausente se leyó como un módulo sin problemas. Ese no lo elegí, salió mientras buscaba antecedentes para otra pregunta, y es uno solo.

La forma ancha la encontré en cuatro casos, todos elegidos: el informe dice lo que se le pidió al control, no lo que logró. Si un filtro seccomp de firejail falla al instalarse, firejail lo avisa una vez, y nada con --quiet, y después lo da por instalado; esa falla no la pude provocar, y lo que comprobé es que ninguna salida para el operador le pregunta al kernel cuántos filtros quedaron instalados; firemon y jailcheck solo leen el modo. El comando ValidateFormat de sbom-tool, de Microsoft, imprime que la validación falló y sale con código 0; en su portal, MSRC lo clasificó como Security Feature Bypass (VULN-229761). npm audit signatures salta a propósito los paquetes que la configuración, al auditar, asigna a un registro sin claves de firma, cuando sus documentos no traen ni firmas ni atestaciones, y no dice cuántos saltó; lo medí en npm 10.9.8. pmg, que solo leí, trae una lista de hosts permitidos en 15 de sus 17 perfiles y nada la aplica.

Un patrón encontrado en casos elegidos por tenerlo no demuestra nada, así que sorteé objetivos. No publiqué la semilla ni el script de ninguno de los dos sorteos. Del segundo consta en público que su marco llegó a GitHub más de diez horas antes de que el objetivo se nombrara. Del primero no hay constancia pública de que la semilla precediera al resultado. El primer sorteo, sobre las 58 herramientas que quedaban en el inventario, dio Capslock de Google, un analizador de capacidades de Go. Lo examiné leyendo su código y su documentación, sin ejecutarlo, y solo en su modo predeterminado. Ahí el patrón no apareció: aborta cuando no puede analizar y reporta como una capacidad aparte casi todo lo que no puede seguir, salvo un punto ciego que documenta. Su modo de comparación, el único que bloquea, no lo examiné. El segundo sorteo, sobre las herramientas que bloquean, dio Bazel, y ahí sí apareció. Con linux-sandbox, la opción --sandbox_block_path salta una ruta que no existe cuando arranca una acción, y no lo dice en ningún nivel de detalle que probé. Si la ruta aparece mientras la acción corre, la acción la lee. Un bloqueo que sí se aplicó también se pierde para la acción en curso cuando, desde fuera del sandbox, se renombra otro archivo encima o se lo borra y se lo vuelve a crear. Lo reporté como documentación, no como vulnerabilidad, en bazelbuild/bazel#31318. El sorteo eligió el objetivo, no el mecanismo. A Bazel le hice las mismas preguntas que a todos, y la de rutas ausentes llevó a esa opción. Que saltara las rutas ausentes se pidió en público en 2018 y llegó en la 7.3.0. Lo que faltaba era decir qué significa.

Son dos casos sorteados, uno con el patrón y otro sin él en el modo examinado. Dos casos no establecen nada. No sé con qué frecuencia aparece esto en una herramienta que nadie eligió, y con esto no se puede saber.

Escribí los límites junto a cada medición. Los marcos son repositorios populares de código abierto, no los proyectos en los que alguien compila; las librerías están sobrerrepresentadas y el código privado no está. En Go los dueños son los del día en que medí, no los del día de cada versión, y no compilé ni ejecuté nada. nono es un solo proyecto. Lo que ejecuté de las auditorías lo ejecuté en Linux; en macOS no repetí nada.

Lo que Go muestra es que se puede cerrar la puerta por la que el código se ejecuta al instalarse sin cambiar cuántos lo publican.