Sobre: El hackeo de $6.3B no fue un bug de código (Dev.to, id 4949662, publicado 30-Oct-2026 21:58:55Z).
Huella: HUELLA-ADDENDUM-CORRECCION-MOTOR-20261030
El artículo afirma que 807 de las observaciones de diez repositorios no son 807 vulnerabilidades, y nombra 228 que provienen de dos códigos del propio instrumento que detectan el mecanismo, no el defecto. Ese párrafo era honesto y sigue siéndolo.
Lo que faltaba era el paso siguiente: si el código detecta el mecanismo y no el defecto, el código estaba mal. Lo corregí. Este addendum publica la cifra exacta, porque corregir en silencio un número ya publicado es la misma clase de fraude que inflarlo.
Qué estaba mal (dos defectos de dominio, no dos umbrales)
- GATE_RC_4_SECRETO_EXPUESTO medía el uso, no la fuga. Marcaba ${{ secrets.* }} en env: —global o de job— y en with:. Esos son los dos mecanismos sancionados para entregar un secreto a un paso sin que entre al cuerpo del script: declararlos hallazgo era marcar como fuga el modo correcto de usarlo.
Dominio corregido: RC 4 mide la interpolación dentro de run:, el único caso en que el valor entra a una línea de shell y puede acabar en un log.
- GATE_RC_2_SUMIDERO_IMPUDO trataba la ausencia de download-artifact como si fuera el sumidero. Un job con id-token: write que no materializa árbol, no invoca herramienta dinámica y no restaura cache no ejecuta nada: el invariante se cumplía por vacuidad y el hallazgo era la forma de un job que no consume.
Dominio corregido: RC 2 mide la presencia del árbol o de sus dependencias (checkout, npx/pipx, o actions/cache en lugar del artefacto —la cache no está autenticada contra el build de la misma corrida, y eso sí es cadena de custodia rota).
El número exacto
antes después
Observaciones de gate 596 401
SC1_* (superficie de permisos) 211 211
Total 807 612
RC 4 (secreto expuesto) 152 3
RC 2 (sumidero impudo) 76 30
RC 1 (autoridad contaminada) 60 60
RC 5 (ref movible) 308 308
Eliminadas: 195 observaciones (149 de RC 4, 46 de RC 2). No 228: 228 era el tamaño de la clase, 195 su contribución — el resto de la clase ya caía por otras conjunciones. Los códigos que sostienen la tesis (RC 1, RC 5, SC1_ROOT_WRITE_INHERITED) no se movieron: la corrección quitó ruido, no criterio.
El delta no está estimado: los 163 workflows del lote se cargaron una sola vez, anclados por blob_sha + sha256, y se corrieron contra el motor anterior y el corregido en la misma corrida.
Y el titular se encoge mientras la tesis se refuerza
807 observaciones, 115 de conjunción → 14.2%.
612 observaciones, 115 de conjunción → 18.8%.
La proporción de lo que el artículo defiende subió 4.6 puntos porque se retiró ruido del denominador. El falsador no se movió: 10 de 10 repositorios siguen con al menos una conjunción de autoridad, y las tres lecturas del falsador (A 10/10, B 6/10, C 4/10) quedan idénticas. Lo que cambió es el instrumento; la tesis sigue en pie sobre el mismo código que antes de la corrección.
El método, para que se pueda repetir o desmontar
Predicción pre-registrada antes de tocar el motor — el censo independiente de la superficie predecía RC 4 = 4 y RC 2 = 30. Medido: RC 4 = 3 (el censo contaba filas de job; RC 4 emite un hallazgo por job, y un job de withastro/astro tenía dos pasos con secreto), RC 2 = 30. La predicción falló por uno y el fallo está publicado en el acta, no borrado.
Reproducción del 807 como control sobre los mismos bytes antes de corregir: 596 + 211 = 807, exacto. Un motor que no reproduce su propia cifra publicada no puede medir su corrección.
El banco de pruebas era parte del defecto. El caso positivo de RC 4 afirmaba RC 4 sobre env: — es decir, el banco avalaba el defecto. Se reescribió, y se añadieron tres controles negativos que hacen fallar el comportamiento viejo: env: sancionado → 0 hallazgos, with: sancionado → 0, job sin árbol ni herramienta dinámica ni cache → 0. --self-test: 8 → 11 casos, RC 0.
Residuo declarado, no escondido: un job con env: que porta secretos y ejecuta código de terceros solo cae bajo RC 1 si además declara scopes de escritura; fuera de ese caso la combinación queda FUERA DE MEDICIÓN. Se acepta el estrechamiento a cambio de retirar 195 observaciones que no eran defecto.
La tasa que faltaba
La TPR de RC 2 y RC 4 ya está medida, y medida sobre el motor vigente. No es una muestra: es un censo de la clase completa, porque la población residual eran 33 hallazgos y muestrear 20 de 30 no habría ahorrado nada.
Antes de abrir un solo cuerpo de job se escribió una predicción pre-registrada, para que el resultado no fuera post-hoc. Después se re-descargaron los 163 archivos de workflow del lote y se verificó su sha256 contra el manifiesto: 163 idénticos, 0 fallos. El motor corregido reprodujo 401 hallazgos de gate exactos sobre esos bytes (RC 1 60 · RC 2 30 · RC 4 3 · RC 5 308). La población está reproducida, no recordada.
lectura pregunta resultado
Formal ¿la forma que el motor reporta existe en el YAML crudo? 33/33, 0 falsos positivos
Control negativo los 46 RC 2 que el motor viejo emitía y el corregido retiró, ¿se leen como sin forma? 46/46, 0 rojos
Sustantiva de esas 33 formas, ¿cuántas son cadena de custodia rota? 5/33 (15.2%)
El control negativo es la parte que importa y la que casi nadie publica: sin él, una TPR alta solo prueba que el lector está de acuerdo consigo mismo.
La cifra que no se puede publicar como «vulnerabilidades» es 612. De los 30 hallazgos RC 2 residuales, 20 emiten solo el motivo actions/checkout — y actions/checkout junto a id-token: write es la forma de todo workflow OIDC correcto: el checkout es requisito del patrón, no su defecto. El falso positivo no desapareció con la corrección: se transformó. Antes el motor leía mal (228 observaciones espurias); ahora lee bien una forma que no es un defecto. Decir «612 vulnerabilidades» seguiría siendo un error, y este addendum no lo dice.
Y 16 de los 33 (48%) son un hallazgo de otra clase, que la propia predicción no había previsto: NO_SUMIDERO. El job declara id-token: write y ningún paso consume esa autoridad. No publica, no firma, no atestigua. La autoridad se concede y no se ejerce. La taxonomía de la predicción no tenía esa clase; se declara como hueco de la predicción, no como hallazgo encajado a la fuerza.
A esa clase no se le asigna severidad, y se declara por qué: su valor está NO_MEDIDO. No sabemos si vale 0 —la autoridad concedida a un job que no la usa es superficie sin acto, y la superficie no es un defecto por sí sola— ni si es el hallazgo más fuerte del lote: un token OIDC de vida corta que nadie consume sigue siendo una credencial emitida contra el registro, y el mínimo privilegio se mide por lo que la credencial puede, no por lo que el YAML usa. Las dos lecturas son defendibles con los mismos bytes y ninguna está medida. Escribir una severidad aquí sería inventarla; escribir 0 sería leer no_medido como cero. Queda abierta y a la vista, en el mismo documento que sí cierra las cifras que pudo cerrar.
Dos defectos del lector se encontraron y se corrigieron antes de publicar cualquier cifra. El primero daba 45/46 en el control negativo, porque un comentario al final de una clave de job (test-dev: # TODO) rompía el ancla. El segundo daba 30/33 en el censo formal, porque un comentario al final de un permiso (id-token: write # OIDC) rompía la prueba. Los dos, de haberse publicado, habrían «demostrado» que el motor estaba mal cuando el error era de quien lo leía. Quedan escritos aquí porque el camino cuenta.
Lo que sigue sin medirse no cambia de forma, solo de tamaño: RC 1 (60), RC 5 (308) y los códigos SC1 (211) siguen sin tasa. Y los cuatro puntos ciegos del instrumento siguen ahí: permisos por defecto del GITHUB_TOKEN, políticas OIDC del registro destino, código nativo no aislado en runtime, y workflows reutilizables alojados en repos ajenos.
Evidencia en el repositorio (platino-labs):
sim/gate_workflows_ci.py (blob bc5f83ca, 711 l)
INVESTIGACION/evidencia/preregistro_correccion_motor_20261030.json (blob 47dfc620)
INVESTIGACION/evidencia/preregistro_tpr_motor_corregido_20261030.json (blob 7e66dac0, la predicción previa)
INVESTIGACION/evidencia/censo_tpr_motor_corregido_20261030.json (blob 2308ecbc, el censo: 33 hallazgos, su clase, su locus y su control negativo)
INVESTIGACION/evidencia/lote_autoridad_10_repos_20261030_post_correccion.json (blob a2b3b5c2)
INVESTIGACION/evidencia/lote_autoridad_10_repos_20261030.json (blob 1bef96a8, la medición previa: superada, no borrada)
!-- HUELLA-TPR-MEDIDA-ADDENDUM-20261030 --><!-- HUELLA-NO-SUMIDERO-MEDIDO-ADDENDUM-20261030 -->
Top comments (0)