DIEGO CECATO / NOTE DAL CAMPO

Il lockfile blocca la versione. Non sempre blocca il codice.

diego cecato//
Copertina dell'articolo: Il lockfile blocca la versione. Non sempre blocca il codice.

Un file con estensione .lock nel repository rassicura. Le versioni sono fissate, la build sembra ripetibile, la pipeline può installare esattamente le dipendenze previste. Poi qualcuno compromette il repository da cui arrivano gli archivi. La domanda diventa meno confortevole: se il server consegna codice diverso sotto la stessa versione, il nostro sistema se ne accorge e si ferma?

È una domanda molto diversa da “abbiamo committato il lockfile?”. E lo è anche da “usiamo HTTPS?”. Il 14 ottobre 2026 Keegan Ryan di Trail of Bits ha pubblicato un test sperimentale su 25 package manager che mette alla prova proprio questo confine. Nel campione e nelle configurazioni esaminate, soltanto due verificavano l’integrità di ogni pacchetto scaricato senza passaggi aggiuntivi. Il numero è interessante, ma sarebbe un errore trasformarlo nella classifica definitiva della sicurezza dei linguaggi.

Il risultato utile è un altro: abbiamo dato lo stesso nome, “gestione delle dipendenze”, a sistemi che fanno promesse molto diverse. E quando le promesse non sono esplicite, è facile costruire controlli che sembrano seri senza verificare la proprietà che ci interessa davvero.

La versione è un indirizzo. L’integrità è una verifica.

Un lockfile può stabilire che il progetto usa una specifica versione di una libreria, risolvere l’albero delle dipendenze e rendere più stabile la riproduzione dell’ambiente. Tutte proprietà importanti. Nessuna, da sola, garantisce che i byte scaricati oggi coincidano con quelli che il team aveva approvato ieri.

Per arrivare a quella garanzia servono almeno due elementi: un riferimento affidabile al contenuto atteso, per esempio un hash crittografico, e un controllo che confronti il contenuto ricevuto con quel riferimento interrompendo l’installazione in caso di differenza. Il secondo verbo conta quanto il primo. Un avviso seguito dall’installazione non equivale a una verifica obbligatoria.

Anche la presenza di un hash può ingannare. Può identificare una revisione, una ricetta di build o un file di metadati senza coprire l’archivio che verrà eseguito. E un hash ottenuto dalla stessa sorgente compromessa del pacchetto non è automaticamente un punto di fiducia indipendente: l’attaccante potrebbe sostituire entrambi.

Bloccare la scelta di una dipendenza e verificare ciò che stiamo eseguendo sono due controlli diversi. Confonderli significa spostare il rischio fuori dalla checklist, non eliminarlo.

Quattro ecosistemi, quattro contratti operativi

npm. Nei progetti JavaScript, un package-lock.json presente e gestito correttamente può contenere gli hash di integrità usati durante l’installazione. Il comando npm ci richiede un lockfile coerente con il manifest e non lo riscrive: è una buona base per la CI. Non significa però che tutte le installazioni npm, in qualsiasi modalità e configurazione, abbiano identiche garanzie. Inoltre npm audit signatures è un controllo distinto sulle firme e sulle attestazioni del registry, non un sinonimo di npm audit per le vulnerabilità note.

Composer. Qui la differenza è più sottile, soprattutto per chi sviluppa in PHP. Un composer.lock può registrare il riferimento a un commit Git. Ma nel caso documentato da Trail of Bits, l’archivio ZIP distribuito tramite l’URL del commit non veniva verificato ricostruendo e confrontando il tree Git. La stringa che identifica il commit non è quindi, di per sé, la prova crittografica dei byte contenuti nello ZIP. Questo non rende inutile Composer né dimostra che ogni installazione sia compromessa; identifica una garanzia che non bisogna attribuirgli senza un controllo aggiuntivo.

NuGet. Nell’ecosistema .NET la situazione distingue ancora una volta il dato dalla policy. Esistono lockfile e meccanismi di firma, ma l’obbligo di accettare soltanto pacchetti firmati da soggetti considerati attendibili richiede una configurazione esplicita. La documentazione Microsoft su nuget.config descrive signatureValidationMode=require e la sezione trustedSigners. Un team può avere una soluzione perfettamente funzionante e, nello stesso tempo, non aver deciso quali firmatari sono autorizzati a fornirgli codice.

Go. Nel test è tra i casi più solidi, insieme a vcpkg, per la verifica dell’integrità dei download. Il punto architetturale è il checksum database di Go: il client non deve fidarsi soltanto della risposta del proxy che gli consegna il modulo, ma può autenticare i checksum attraverso un registro verificabile. Anche qui esistono eccezioni: moduli privati e variabili come GONOSUMDB o GOSUMDB=off cambiano il perimetro. Dire “Go verifica” senza parlare della configurazione sarebbe ripetere lo stesso errore che lo studio invita a evitare.

Come è stato fatto il test, e cosa non dimostra

Ryan ha costruito un laboratorio con container Docker e un proxy capace di modificare le risposte dei repository, simulando un server di distribuzione compromesso. Il package manager scaricava la dipendenza; il test osservava se il contenuto alterato arrivava fino all’esecuzione oppure veniva respinto. È un modello di minaccia concreto: non un autore malevolo che pubblica una nuova release, ma un intermediario della distribuzione che cambia ciò che viene servito.

La metodologia ha limiti dichiarati: una versione del gestore, un pacchetto di prova e una piattaforma per ciascun caso, spesso Ubuntu, con impostazioni vicine ai percorsi introduttivi. Non copre ogni combinazione di client, sistema operativo, policy aziendale e repository privato. Non prova neppure che un attacco sia avvenuto nel mondo reale contro quei progetti. Il suo valore sta nel mostrare che le impostazioni ordinarie non producono sempre le garanzie che gli sviluppatori immaginano.

C’è anche una distinzione da non perdere: il test non è una graduatoria dei bug dei package manager. Alcuni comportamenti sono scelte progettuali documentate, magari nate per favorire aggiornamenti o compatibilità. La domanda operativa non è chi abbia il bollino migliore, ma se la scelta di default coincide con la politica di rischio della nostra applicazione.

HTTPS, checksum, firma e provenienza non sono quattro nomi per la stessa cosa

HTTPS protegge il trasporto verso l’endpoint con cui stiamo comunicando. È indispensabile, ma non risolve il caso in cui l’endpoint legittimo sia compromesso. Un checksum confronta il contenuto con un valore atteso, ma richiede che quel valore sia stato ottenuto attraverso un percorso affidabile. Una firma lega un artefatto a una chiave; resta da decidere di chi sia la chiave e perché il progetto debba fidarsene.

Un transparency log aggiunge un’altra proprietà: rende osservabili gli eventi di firma e aiuta a individuare emissioni inattese. È la direzione esplorata da Sigstore e Rekor. Non basta però che una firma compaia in un registro: occorre verificare l’identità prevista, la firma, il contenuto e le prove del registro, secondo una policy coerente. La trasparenza senza verifica resta una traccia consultabile, non una barriera automatica.

Queste garanzie si sommano, non si sostituiscono. Un artefatto può avere un hash corretto ma provenire da un autore che non avremmo autorizzato. Può essere firmato da una chiave valida ma appartenere a un processo di rilascio compromesso. Può arrivare via TLS perfetto da un repository che distribuisce byte inattesi. La sicurezza della supply chain vive nei legami tra identità, contenuto, distribuzione e decisione di installare.

La CI dovrebbe dimostrare il rifiuto, non soltanto il successo

La conseguenza pratica è abbastanza scomoda: la pipeline verde prova che una certa installazione è riuscita, non che avrebbe rifiutato una dipendenza manomessa. Per conoscere la seconda proprietà serve un test negativo, eseguito in ambiente isolato e senza introdurre codice non fidato nella pipeline reale.

  • Definire il modello di minaccia. Ci interessa un repository compromesso, un pacchetto pubblicato da un maintainer malevolo, una chiave rubata o un proxy interno alterato? Le contromisure cambiano.
  • Verificare il percorso effettivo. Versione del package manager, opzioni usate in CI, presenza e integrità del lockfile, modalità frozen/locked, registry e mirror aziendali. Non basta controllare un file nel repository.
  • Controllare cosa provoca un errore. Un hash non corrispondente interrompe davvero la build? Una firma mancante viene rifiutata o produce soltanto un warning? La policy dei firmatari è configurata?
  • Proteggere la fonte del valore atteso. Se il checksum o la chiave arrivano dallo stesso sistema che potrebbe essere compromesso, capire quale garanzia indipendente resta.
  • Ripetere il test dopo gli aggiornamenti. Cambiano i default, le versioni dei client, i formati dei lockfile e le eccezioni per le dipendenze private. Il controllo va trattato come una proprietà osservata, non come una decisione presa una volta per tutte.

Qui torna utile un tema che avevo affrontato parlando del costo nascosto della manutenzione open source: sapere quali dipendenze usiamo è soltanto una parte dell’inventario. Dobbiamo anche sapere chi le mantiene, da dove arrivano e quale processo decide che il codice ricevuto è accettabile. Non è burocrazia aggiunta allo sviluppo; è progettazione del confine di fiducia.

La vera dipendenza è dalla promessa che non abbiamo verificato

L’aspetto più interessante dello studio non è il confronto fra venticinque strumenti. È la distanza fra il significato che attribuiamo a un artefatto e il comportamento reale del sistema che lo consuma. Scrivere un hash non significa verificarlo. Verificare una firma non significa aver scelto il firmatario giusto. Bloccare una versione non significa bloccare i byte. Sono passaggi distinti e ciascuno può fallire senza che gli altri se ne accorgano.

Quando progettiamo una filiera software, il risultato da chiedere non è “abbiamo un lockfile?”. È “quale modifica non autorizzata sapremmo rilevare, in quale punto, e quale componente fermerebbe la distribuzione?”. Finché non sappiamo rispondere con un test riproducibile, la nostra sicurezza delle dipendenze assomiglia troppo a una dichiarazione di intenti. Ben formattata, versionata e magari persino firmata. Ma pur sempre una dichiarazione.

Domande frequenti

Un lockfile garantisce che una dipendenza non sia stata alterata?

Non necessariamente. Deve contenere un riferimento crittografico affidabile al contenuto e il package manager deve confrontarlo con i byte scaricati, bloccando l’installazione in caso di differenza. I comportamenti cambiano tra ecosistemi e configurazioni.

Che differenza c'è tra checksum e firma di un pacchetto?

Il checksum confronta il contenuto con un valore atteso. La firma aggiunge un legame crittografico con una chiave, ma serve una policy per stabilire quali firmatari siano attendibili. Nessuno dei due controlli sostituisce automaticamente l’altro.

Usare npm ci basta per mettere al sicuro la CI?

npm ci impone coerenza con package-lock.json e non aggiorna il lockfile, ma non copre ogni minaccia alla supply chain. Occorre verificare le policy del registry, gli hash effettivi, le firme quando applicabili e il comportamento della build davanti a pacchetti alterati.

Perché Go è risultato diverso dagli altri package manager nel test?

Nella configurazione studiata da Trail of Bits, Go usa un checksum database autenticato che fornisce una verifica indipendente dal solo proxy di download. Le impostazioni per moduli privati e la disattivazione di GOSUMDB possono cambiare la garanzia.