Tornar al Blog

La protecció de branca va funcionar. Va durar cinc segons

La protecció de branca va funcionar. Va durar cinc segons

A les 10:08:29 UTC del 21 d'octubre, la protecció de branca de GitHub va fer exactament allò per a què estava posada: rebutjar una modificació no autoritzada del flux de publicació de LiteLLM, perquè no havia passat la revisió ni les comprovacions obligatòries. A les 10:08:34 —cinc segons més tard— una credencial d'un compte de desenvolupador amb privilegi va trepitjar aquella protecció i va aplicar el canvi. Una hora i mitja després hi havia set paquets enverinats a PyPI. No va fallar cap control. Es van complir tots tres.

LiteLLM és una passarel·la que es posa davant de diversos models de llenguatge per parlar amb tots amb la mateixa crida. Si a la teva empresa algú ha muntat alguna cosa amb IA l'últim any, val la pena que miris si és per allà sota, encara que no hagi aparegut en cap conversa de direcció. El 21 d'octubre va aparèixer a PyPI i a Docker una versió 1.107.0 que no havia fet el projecte, amb un lladre de credencials a dins. I l'equip va publicar després una cosa que gairebé ningú no publica: la cronologia amb segons del que va passar dins de la seva pròpia canonada de publicació.

Aquell document és el que és interessant, i no pel paquet. És un plànol, minut a minut, de com es travessa una cadena de publicació que tenia posats els controls que recomana tothom. Nosaltres no el vam llegir pensant en Python; el vam llegir pensant en el GitLab, el Jenkins o l'Azure DevOps de qualsevol de les empreses de programari a qui portem la infraestructura.

Els cinc segons, llegits de l'informe oficial

Aquesta és la seqüència tal com la publica el mateix projecte, en horari UTC del 21 d'octubre del 2026. No hi ha res aquí que no sigui al seu avís.

Hora UTC Què passa
10:08:29La protecció de branca rebutja una modificació no autoritzada del flux de publicació privat, per no haver passat la revisió ni les comprovacions obligatòries.
10:08:34La credencial del compte que és propietari de l'organització de GitHub anul·la aquelles proteccions i aplica el commit.
10:46:39Arrenca el flux modificat, que ara compila des d'un repositori de l'atacant amb un nom gairebé idèntic al del projecte.
11:08:09Acaba l'execució de proves. Fins aquí, tot ha seguit el procediment.
11:08:53Comença la canonada de producció.
11:11:18Una segona credencial de desenvolupador satisfà la porta d'aprovació.
11:39:51 – 11:40:13Es pugen a PyPI set paquets maliciosos de litellm==1.107.0.
11:46:56Les tasques de publicació de Docker corren amb el contingut maliciós.
11:53:49 – 11:54:01Una tercera credencial esborra els registres del flux de treball.
13:23 / 13:51 / 14:20PyPI retira la versió; es confirmen no disponibles les etiquetes de GHCR; es retiren o reapunten totes les etiquetes afectades de Docker Hub.

Llegeix-ho un altre cop fixant-te només en els tres moments en negreta. Són tres credencials diferents de tres persones diferents, usades en tres punts diferents de la cadena, i cadascuna fa una cosa perfectament permesa per a qui la té: saltar-se la protecció de branca, aprovar un desplegament a producció, netejar registres d'execució. L'atacant no va trencar res. Va omplir formularis.

Tres controls, una sola pregunta

Aquí hi ha el que ens enduem, i és incòmode perquè s'assembla molt al que tenim muntat gairebé tots. Aquells tres controls no són tres capes: són la mateixa capa repetida tres vegades. La protecció de branca pregunta «tens credencial amb privilegi per anul·lar-me?». La porta d'aprovació pregunta «hi ha una segona credencial de desenvolupador que digui que sí?». I el registre d'auditoria dona per fet que qui l'escriu no és qui ataca. Les tres preguntes són la mateixa pregunta: ets qui dius ser? I la resposta era que sí, tres vegades, perquè les tres identitats estaven robades.

I hi ha un quart control que tampoc no apareix a la cronologia, perquè no es va arribar a disparar. El document de la cadena d'atac ho diu de passada, amb una frase que val una auditoria sencera: «els tokens de la CLI de GitHub són post-2FA i no estan subjectes als fluxos de reautenticació de segon factor de "superusuari" de GitHub». És a dir: el segon factor, que és el que la majoria de nosaltres posem sobre la taula quan ens pregunten si la cadena està protegida, estava actiu i no hi tenia res a dir. Ja s'havia complert el dia que algú es va autenticar al seu propi portàtil.

Tres portes seguides no són defensa en profunditat si totes tres porten el mateix pany. El que cal són portes que preguntin coses diferents, que és exactament del que va el Zero Trust quan s'aplica de debò i no com a etiqueta comercial. La pregunta que faltava a tota la cadena, i que cap control no va fer en cap moment, és la més simple de totes: aquest canvi és el que volíem fer? Ningú no va comparar el flux de publicació que es va executar contra el flux de publicació aprovat. Si ho haguessin fet, la línia que apuntava a un repositori amb un guió de més al nom de l'organització hauria cantat sola.

L'engany va ser el mateix truc dues vegades, i totes dues a un caràcter de distància. El flux compilava des d'una organització de GitHub amb un nom gairebé igual al de la real. I les dades robades, segons l'anàlisi tècnica de StepSecurity, sortien cap a un domini que tampoc no és el del projecte: acaba en .dev quan el de debò acaba en .ai. Cap de les dues coses no la detecta un control que només mira identitats.

D'on van sortir les credencials (la part que ens va tocar de prop)

L'avís explica l'origen amb una frase que convé llegir a poc a poc: «Claude Code incrusta el token OAuth de la CLI de GitHub de l'usuari en un cos de resposta, i en situacions en què els cossos de resposta es registren, aquests tokens poden aparèixer als registres de LiteLLM». L'atacant va entrar en un desplegament intern de LiteLLM exposat a Internet i en va treure els tokens: primer el del propietari de l'organització i després, el mateix dia, els de dos enginyers amb permisos de mantenidor.

Aquella és la frase que t'hauries d'endur a casa teva, perquè no té res a veure amb LiteLLM. Si has posat una passarel·la davant dels models —i gairebé tothom que fa alguna cosa seriosa amb IA acaba posant-ne una, per control de cost o per no repartir claus— aquella passarel·la registra les peticions i les respostes. És la seva feina. El dia que una eina de les que hi parlen fica una credencial dins d'una resposta, la teva taula de registres es converteix en un magatzem de secrets que no vas declarar, que ningú no rota, que probablement no està xifrat en repòs i al qual té accés molta més gent de la que té accés al teu gestor de contrasenyes. Ja vam escriure sobre l'altra cara d'aquest mateix problema quan vam veure que l'agent d'IA no surt al teu registre: surt qui li va deixar el token.

Dos dies després de l'incident, el 23 d'octubre, el projecte va publicar un segon document amb l'arrel de tot, i és més dura que l'anterior. El 27 de maig del 2026 es va pujar a un repositori públic —LiteLLM-Labs/lite-harness— un fitxer de configuració que contenia la clau mestra d'un desplegament de la seva plataforma d'agents. Ho expliquen ells mateixos: «l'error es va identificar ràpidament, es va reescriure el commit per sobre, i vam identificar les claus exposades com a pendents de revocació. Tanmateix, la revocació de la clau mestra no es va arribar a produir». El 10 d'octubre, 136 dies després, l'atacant va fer servir aquella clau per entrar a la plataforma i treure'n el token. D'aquell token el mateix document en diu dues coses més: que «no estava associat a un compte de servei i no tenia l'abast retallat», i que era el del compte del propietari de l'organització de GitHub.

El detall que canvia el triatge: no cal fer servir la llibreria

Gairebé tothom, en assabentar-se'n, va fer la pregunta equivocada: «alguna de les nostres aplicacions fa servir això?». No és aquella. Segons el desmuntatge tècnic de StepSecurity, el paquet enverinat ficava a site-packages tres fitxers, i un d'ells és el que canvia el problema de lloc: cook.pth, de dotze bytes, el contingut sencer del qual és import cook.

Un fitxer .pth és un mecanisme vell i legítim de Python: el mòdul site, en arrencar l'intèrpret, executa qualsevol línia d'un .pth que comenci per import. Abans del teu codi. A cada arrencada de l'intèrpret. Tant se val si llances l'aplicació, si executes pip, si corres els tests, si fas un python -c de tres línies per comprovar una cosa. Ningú no ha d'importar litellm mai. N'hi ha prou que el paquet estigui instal·lat en aquell entorn virtual.

Això mou el radi d'abast de les aplicacions d'IA a qualsevol màquina on algú fes un pip install per provar: el portàtil del desenvolupador que ho va mirar un divendres, el contenidor efímer d'integració contínua, la màquina d'algú en pràctiques, l'entorn virtual de la prova de concepte que es va quedar encesa. I el que el lladre recull, sempre segons StepSecurity, no és poc: contrasenyes i galetes del navegador, claus de ~/.ssh, claus GPG, fitxers .pem, credencials d'AWS, Azure i GCP, configuracions de kubectl i Docker, tokens de la CLI de GitHub, credencials d'eines d'IA, la taula de credencials de la mateixa passarel·la, i —això és el que menys es comenta— l'entorn de tots els processos en execució llegit de /proc. És a dir, també les variables d'entorn de serveis que no tenen res a veure amb Python.

El tercer fitxer, sitecustomize.py, deixa un procés de fons que cada hora es descarrega un tros de codi d'un servidor i l'executa. El servidor no està escrit a foc: el llegeix d'un fitxer de text allotjat en un repositori públic, per poder canviar-lo quan vulgui. Això ja no és un paquet maliciós; és una porta oberta amb manteniment inclòs.

La finestra real: dues hores i quaranta minuts

La pregunta operativa és més concreta que «fem servir la llibreria» i que «la fèiem servir en aquella època». què es va descarregar la teva infraestructura entre les 11:39:51 i les 14:20 UTC del 21 d'octubre. A PyPI els paquets van estar vius des de les 11:39:51 fins a les 13:23. A Docker Hub i GHCR, les etiquetes v1.107.0, latest, main-latest i main-stable van apuntar a la imatge enverinada des de les 11:46:56 fins a les 14:20. En hora peninsular això és de les 13:39 a les 16:20: mitja tarda d'un dimecres laborable.

Dues hores i quaranta minuts de finestra total. Si les teves compilacions corren de matinada, el més probable és que te n'escapessis per horari. Si compiles a cada fusió a la branca principal, aquella tarda vas compilar diverses vegades. Aquella diferència —que no té res a veure amb la seguretat i tot amb a quina hora corre el teu robot— va decidir qui es va endur el paquet i qui no. Ens sembla un bon recordatori de com de poc determinista és això: no et va salvar la teva política de dependències, et va salvar el cron.

El que NO és la lliçó

La reacció automàtica de mig sector va ser «cal fixar les versions». Fixar versions està bé i s'ha de fer, però aquí cal dir-ho amb més precisió, perquè dit així és fals consol: 1.107.0 era una versió fixable. Qualsevol que tingués un robot d'actualització de dependències configurat per obrir i fusionar automàticament les pujades de versió menor es va endur el paquet enverinat a través del seu fitxer de blocatge, no malgrat ell. La teva versió fixada val exactament el que valgui el procés que té permís per canviar-la.

La segona reacció va ser «nosaltres fem servir contenidors, no instal·lem paquets a mà». També falla: les etiquetes latest i main-stable són referències mòbils, és a dir, pins que no són pins. Qui va reconstruir una imatge aquella tarda apuntant a main-stable es va endur el contingut maliciós sense canviar ni una línia del seu fitxer de construcció. El que de debò ancora una imatge és el digest, no l'etiqueta, i gairebé ningú no el fa servir perquè obliga a tocar el fitxer cada vegada que vols actualitzar. Aquell és el preu real, i cal decidir-ho sabent quant costa.

I ara la part que ens costa diners dir: a una empresa de vint persones no li recomanarem que es munti el seu propi mirall de PyPI. És car de mantenir, es queda desactualitzat, i el dia que falla ho fa en silenci. El proporcionat per a aquella mida són tres coses avorrides: una quarantena de 24 o 72 hores abans d'adoptar qualsevol versió nova (que en aquest cas t'hauria salvat del paquet de PyPI, que va viure menys de dues hores; de les etiquetes mòbils de Docker, no), l'inventari de quins paquets i quines imatges entren de debò a les teves compilacions, i treure-li al robot de dependències el permís de fusionar sol. Les tres són d'un dia de feina, no d'un projecte. El mirall propi és per a qui compila cent vegades al dia, i llavors sí.

Set comprovacions, amb les ordres

Això és el que faríem nosaltres, en aquest ordre, en un parc on hi hagi Python en algun lloc. Els indicadors són els publicats per StepSecurity i pel mateix projecte. Abans de començar, un avís que té conseqüències: en una màquina sospitosa no arrenquis amb pip install ni amb pip uninstall. Cada invocació de Python dispara la càrrega. S'esborra des de la shell.

  1. Busca el fitxer, no l'aplicació. find / -name cook.pth -path "*site-packages*" 2>/dev/null, i passa'l per totes les màquines amb Python, no només pels servidors de l'aplicació: portàtils, corredors d'integració contínua, màquines de laboratori. El fitxer són dotze bytes; l'inventari és el car.
  2. Mira si el procés de fons continua viu. ls -la /tmp/remote_code_loader.lock i lsof /tmp/remote_code_loader.lock per trobar qui el té agafat, i matar-lo. El lladre hi deixa a més un marcador, /tmp/.pc_once_72129118c59f5636c0bb, quan ha aconseguit enviar les dades; que no hi sigui no demostra res.
  3. Si apareix, no ho netegis: refés l'entorn. La recomanació de StepSecurity és explícita, recrear l'entorn virtual o la imatge des de zero és millor que netejar-la in situ. Si per alguna raó has de netejar, van els tres fitxers i també el __pycache__ corresponent.
  4. Rastreja la versió als teus fitxers i al teu mirall intern. grep -rniE "litellm[=<>~! ]*1\.107\.0" . --include='*.txt' --include='*.toml' --include='*.lock' --include='Pipfile*' sobre els repositoris, inclosos els fitxers de blocatge: és per allà per on va entrar. I després mira si la teva memòria cau o el teu mirall de paquets en va guardar una còpia, perquè les retirades de PyPI no buiden la teva memòria cau.
  5. Compara digests d'imatges, no etiquetes. docker images --digests | grep -E 'berriai/litellm' i creua amb els digests publicats com a compromesos. Si vas reconstruir alguna cosa aquella tarda apuntant a latest o main-stable, assumeix que va entrar.
  6. Si hi va haver exposició, rota com si s'ho hagués endut tot. El mateix avís ho diu sense embuts: el rol de la instància o de la tasca, totes les claus de proveïdor, la contrasenya de la base de dades, i també la clau mestra i les claus virtuals de la mateixa passarel·la, que és per on va començar tot. Afegeix-hi les claus SSH i GPG dels usuaris d'aquelles màquines i les credencials desades al navegador. Rotar mig parc deixa la sensació d'haver actuat: apunta què queda sense rotar i per què, amb data.
  7. Mira enrere al registre del teu núvol. L'avís recomana revisar CloudTrail; val l'equivalent que facis servir. El que busques és ús d'aquelles claus després de les 11:39 UTC del 21 d'octubre des de llocs que no són els teus. I comprova quants dies de registre guardes de debò, perquè aquella finestra és la que decideix si aquesta comprovació es pot fer o no.

I una vuitena, que no és tècnica i és la que de debò canvia alguna cosa: seu amb qui porta la vostra canonada de publicació i pregunteu-li tres coses. Qui es pot saltar la protecció de branca i si queda rastre fora del sistema que el mateix privilegi pot esborrar. Si la porta d'aprovació la satisfà una persona o una credencial, perquè no és el mateix i gairebé sempre està muntat com el segon. I si algú compara el que s'executa amb el que es va aprovar. Si les tres respostes són incòmodes, ja tens la feina de la setmana vinent. El patró no és nou: ja el vam veure al costat d'npm quan vam explicar allò dels 444 paquets d'npm i res a pedaçar, i al servidor de compilació quan va resultar que el servidor d'integració contínua guarda les claus de producció.

El que ens enduem

Cal dir una cosa a favor d'aquest equip: van publicar la cronologia amb segons, inclosa la part on la seva pròpia protecció va ser anul·lada i els seus propis registres van ser esborrats. Això no es fa per gust, i gairebé ningú no ho fa. Bona part del que sabem d'aquest incident ho sabem perquè ells ho van escriure.

I el que queda quan apartes Python, la IA i el paquet concret és una frase que val per al teu hipervisor, la teva còpia de seguretat i la teva consola de gestió igual que per a la teva canonada de publicació: un control que només verifica identitat val zero el dia que la identitat és el que han robat. No perquè estigui mal posat, sinó perquè està contestant una pregunta que l'atacant ja sap respondre. Els cinc segons del 21 d'octubre no són una anècdota curiosa: són el temps que triga un control ben configurat a tornar-se decoratiu.

Nosaltres això ho mirem com a part de la consultoria: inventari del que entra a les teves compilacions, qui té quin privilegi sobre la cadena, i què queda registrat fora de l'abast d'aquell privilegi. Aquell últim punt és el que més vegades hem vist en blanc.

I un apunt de detecció, perquè aquí no hi ha cap fitxer rar a trobar: el que delata aquella màquina és un procés que es desperta cada hora, en un equip on ningú no va programar res. Això ho agafa una eina que mira comportament, és a dir EDR/MDR, i no un antivirus de signatures.

Una última honestedat: no hem vist aquest paquet en cap entorn dels que operem, i no donem xifra de quantes empreses se'l van endur perquè el que es compta en aquests casos són descàrregues, que no és el mateix que empreses. Tot el que afirmem més amunt surt dels dos documents que va publicar el mateix projecte i del desmuntatge tècnic de StepSecurity; on ells escriuen «creiem», nosaltres no hi hem posat «va ser».

Fonts (verificades): cronologia UTC, anul·lació de la protecció de branca, credencials implicades, artefactes afectats i passos de remediació — avís oficial del projecte, docs.litellm.ai; anàlisi tècnica dels fitxers cook.pth, cook.py i sitecustomize.py, abast del robatori, digests i indicadors i ordres de detecció — StepSecurity; fil públic de l'incident — BerriAI/litellm #47826. Origen de la cadena: el commit del 27 de maig del 2026 amb la clau mestra, la revocació que no es va produir, el token sense abast retallat del propietari de l'organització i l'apunt sobre els tokens de la CLI de GitHub i el segon factor — registre canònic de la cadena d'atac, publicat pel projecte el 23-10-2026.

Saps qui es pot saltar els controls de la teva cadena de publicació?

Fem aquell inventari: què entra a les teves compilacions, qui té privilegi sobre la cadena i què queda registrat fora del seu abast. Surt en una tarda i acostuma a descobrir dues o tres coses que ningú no sabia que estaven així.

Parlar amb everyWAN

Etiquetes:

Compartir:

Subscriu-te al nostre butlletí

Per rebre històries del món IT, novetats d'everyWAN i ofertes exclusives per a subscriptors, dona't d'alta a la nostra llista de correu

everyWAN
everyWAN