Dipendenze npm fuori controllo: i 4 costi sui frontend enterprise
Nel 2025 Sonatype ha contato 454.600 nuovi pacchetti malevoli e il 99,8% del malware del quarto trimestre veniva da npm. Quanto costa e come lo teniamo a freno.
In questo pezzo
La proliferazione delle dipendenze npm è la distanza fra i pacchetti che un team frontend ha scelto e quelli che installa davvero: un package.json con 40 voci che nel lockfile diventano diverse centinaia, tirate dentro da qualcos'altro. Nessuno le ha approvate. Nessuno legge le loro note di rilascio. Finiscono in produzione comunque.
Il danno più grosso lo fa sui frontend che durano anni. Una dashboard interna. Un portale clienti. Un design system che usano altri tre team. Il codice si stabilizza, l'albero sotto continua a muoversi, e a ogni installazione qualcuno decide per te: sono i manutentori dei pacchetti, con cui non hai mai parlato.
Quanto costa davvero un albero di dipendenze fuori controllo
Una superficie d'attacco che nessuno ha scelto
Il 2025 ha chiuso la discussione sul fatto che il rischio sia teorico. L'8 settembre un manutentore è caduto in un phishing su un finto dominio di assistenza, npmjs.help, e 18 pacchetti sono usciti in versioni malevole, fra cui chalk e debug, con circa 2,6 miliardi di download settimanali alle spalle (Wiz). Il codice malevolo girava nel browser, aspettava window.ethereum e riscriveva la destinazione delle transazioni crypto. Quel giorno le piattaforme di hosting hanno passato le build al setaccio: il registro di Vercel racconta bene com'è andata.
Una settimana dopo è arrivato Shai-Hulud, il primo malware npm che si replica da solo. Cercava segreti sulla macchina infetta e usava qualunque token npm trovato per pubblicare versioni avvelenate di tutti i pacchetti raggiungibili con quel token. GitHub ha rimosso più di 500 pacchetti (GitHub) e il 23 settembre la CISA ha pubblicato un'allerta (CISA).
Nel 2025 Sonatype ha registrato 454.600 nuovi pacchetti malevoli sui registri pubblici, e il 99,8% del malware intercettato nel quarto trimestre veniva solo da npm (Sonatype). In un'azienda il repository frontend è di solito il più grande consumatore di npm, quindi si porta addosso la maggior parte di quel rischio.
Il rumore degli audit insegna a saltare gli audit
Endor Labs ha misurato che il 95% delle dipendenze vulnerabili di un progetto tipo è transitivo, e che solo il 9,5% circa delle vulnerabilità è sfruttabile a livello di funzione (Endor Labs). Questo rapporto spiega che fine fa npm audit in un repository gonfio. Il team riceve 200 avvisi, risolve la dozzina su cui ha controllo e impara che il numero nel terminale è decorazione. Quando arriva quello vero, l'abitudine di ignorarlo c'è già.
Aggiornamenti bloccati da pacchetti che non hai scelto
Due dipendenze dirette che fissano intervalli incompatibili dello stesso pacchetto transitivo sono il normale punto di arrivo. Una libreria di grafici ferma su peer di React vecchi. Una libreria di form che porta il proprio fork di un validatore. L'aggiornamento non lo blocca il tuo codice: lo blocca un disaccordo fra manutentori, e l'arbitro sei tu. È il costo che si presenta come un trimestre slittato, e la causa nel manifest non si vede.
Tempi di installazione e peso rilasciato
Le installazioni a freddo e i minuti di CI crescono con l'albero, non con il tuo codice. Anche il costo delle review: una riga cambiata in package.json arriva con un diff di 2.000 righe di lockfile che nessuno legge. E lo stesso peso finisce nel browser. Una libreria di date che porta tutte le lingue quando il prodotto ne vende tre, dove Intl.DateTimeFormat formatta gli stessi valori a costo zero, è una decisione presa una volta e pagata a ogni caricamento. Ne abbiamo scritto parlando dei punteggi Lighthouse che affondano.
Perché la pulizia annuale non regge
La risposta consueta è una pulizia una volta all'anno. Non funziona per un motivo meccanico: la potatura agisce sulle 40 righe che controlli, mentre il problema vive nelle diverse centinaia che non controlli. Togli quattro dipendenze dirette inutilizzate e l'albero si muove appena. Lunedì prossimo un aggiornamento minore di un build tool ne rimette trenta, e la review che l'ha approvato ha visto una riga sola.
Fissare tutto sbaglia nella direzione opposta. Se congeli l'albero smettono di arrivare anche le patch, ed è così che un avviso del 2024 sopravvive in una build del 2026.
Quello che funziona non è affascinante: rendere visibile l'albero, rallentarlo, e togliere i privilegi che un pacchetto ottiene gratis al momento dell'installazione.
Misura l'albero, non il manifest
Tre numeri, calcolati in CI e stampati su ogni pull request:
- le dipendenze dirette, da
package.json - i pacchetti risolti in totale, dal lockfile (
npm ls --all --parseable | wc -l) - i pacchetti che eseguono uno script durante l'installazione
Il terzo è quello che anticipa gli incidenti, e quasi nessuno lo segue. Una pull request che muove uno dei tre di più di qualche punto percentuale deve una frase di spiegazione a chi la rivede.
Metti un tempo di attesa su ogni installazione
Oggi è la modifica con il rapporto migliore fra valore e fatica, e si scrive in una riga di configurazione. Le versioni malevole vengono intercettate quasi sempre in poche ore. Il danno si concentra nella finestra fra pubblicazione e rimozione, e una CI che installa a ogni push ci finisce dentro in pieno. Ormai ogni package manager ha il suo ritardo:
min-release-agesu npm, in giorni, da npm 11.10.0 (documentazione npm, la PR che lo introduce)minimumReleaseAgesu pnpm, in minuti, attivo per default a 1440 (un giorno) da pnpm 11 (documentazione pnpm)npmMinimalAgeGatesu Yarn--minimum-release-agesu Bun
Sulle applicazioni usiamo sette giorni. Il filtro vale anche sulla risoluzione transitiva, non solo sulle scelte dirette: il pacchetto che ti compromettono è proprio quello che non hai scelto. Il compromesso va detto chiaro. Anche una patch di sicurezza vera aspetta sette giorni. Tieni una scappatoia, porta la finestra a zero per quella singola installazione, spiegala nel commit e rimetti il numero nella stessa pull request.
Disattiva gli script di installazione per default
Entrambi gli attacchi del 2025 sono passati da uno script di installazione. Su un frontend moderno quel passaggio serve per la compilazione nativa e per pochissimo altro. In CI usa npm ci --ignore-scripts e tieni una lista corta dei pacchetti che compilano davvero qualcosa. Su pnpm la stessa scelta si scrive come onlyBuiltDependencies.
Mettiti in conto due o tre pacchetti rotti la prima volta. In genere scaricano un binario già compilato durante l'installazione. Sono esattamente quelli da guardare due volte.
Smetti di tenere token di pubblicazione a lunga scadenza
Se il team pubblica qualcosa di suo, un design system, un SDK interno, una CLI, a dicembre il terreno si è spostato. Il 9 dicembre 2025 npm ha revocato tutti i token classici, e npm login ora restituisce una sessione di due ore invece di una credenziale durevole (changelog GitHub). Dal 3 febbraio 2026 i token granulari in scrittura hanno una durata massima di 90 giorni (changelog GitHub).
Il trusted publishing via OIDC elimina il segreto salvato: il workflow dimostra la propria identità e npm emette una credenziale che vive per quella singola esecuzione. Shai-Hulud si propagava proprio grazie a un token rubato, quindi qui non si parla di adempimenti: si chiude quel buco preciso.
Dai al repository un budget di dipendenze
Una entra, una esce, e si verifica in review. La domanda su una pull request che aggiunge un pacchetto non è se il pacchetto sia buono. È che cosa sostituisce. Tre controlli prima di dire sì:
- Quante voci aggiunge al lockfile?
npm i <pkg> --dry-runrisponde in pochi secondi. - Esegue uno script di installazione?
- Che cosa scriveremmo al suo posto, e quanto ci vorrebbe?
Se la risposta onesta alla terza domanda è meno di un giorno e il pacchetto porta quaranta voci, scriviamo il codice. Se invece parliamo di un parser, di una libreria per i calcoli sulle date, di una primitiva crittografica, prendiamo la dipendenza e ci teniamo la manutenzione. Il criterio è quanta parte dell'albero il team riesce davvero a leggere.
Dai priorità alla raggiungibilità, non al numero di avvisi
Con quel 9,5% davanti, ordinare la lista delle vulnerabilità solo per gravità butta via quasi tutto il lavoro. La versione economica dell'analisi di raggiungibilità è npm why <pacchetto>: dice quale dipendenza diretta ha tirato dentro quella cosa, che è l'unico punto da cui può arrivare una correzione. Poi la domanda è se il tuo codice arrivi mai alla funzione vulnerabile. Se non ci arriva, l'avviso finisce in una lista scritta con una data di ricontrollo, non in questo sprint.
Come funziona su un frontend che gestiamo noi
Questo sito gira su Next.js con un design system in CSS puro. Togliere Tailwind dalla produzione ha levato dall'albero una dipendenza di build e la sua catena di plugin: era partita come scelta di performance, si è rivelata anche una scelta di sicurezza. Il lavoro lato server passa dai Server Actions al posto di una libreria client per le API. Conta anche il runtime: Bun e Node risolvono e mettono in cache le installazioni in modo diverso, e oggi entrambi supportano il filtro sull'età delle versioni.
Non è un esercizio di qualità del codice. Un albero di dipendenze cresce per inerzia e si riduce solo se qualcuno lo decide, quindi un team che non ha mai deciso quanto grande debba essere ha già deciso.
Domande frequenti
Quante dipendenze npm sono troppe per un frontend?+
Non esiste una soglia valida per tutti i progetti, e qualsiasi numero che leggi è la media di qualcun altro. Il test utile è la leggibilità: una sola persona del team riesce a nominare tutte le dipendenze dirette e a dire cosa fanno? Con quaranta voci dirette di solito sì, con novanta no. Sull'albero risolto guarda l'andamento, non il valore assoluto: un lockfile cresciuto del 30% in un trimestre mentre il prodotto è rimasto fermo è il segnale su cui intervenire.
Un tempo di attesa sulle versioni ritarda di una settimana le patch di sicurezza?+
Sì, ed è il compromesso che accetti. Una finestra di sette giorni rimanda fino a sette giorni una correzione vera. Il rischio che togli resta più grande di quello che aggiungi: il malware pubblicato viene intercettato in poche ore, mentre le vulnerabilità dietro la maggior parte degli avvisi esistono già da mesi quando qualcuno le documenta. Tieni una deroga scritta: porta la finestra a zero per quella singola installazione, spiega il motivo nel commit e rimetti il valore nella stessa pull request.
Per questo pnpm è più sicuro di npm?+
pnpm ti mette in mano impostazioni di partenza più severe. Dalla versione 11 applica un'età minima di un giorno senza che glielo chieda nessuno, e non lascia usare a un pacchetto qualcosa che non ha dichiarato, quindi un import transitivo non dichiarato si rompe invece di funzionare per caso. Con npm arrivi allo stesso punto configurandolo a mano, dalla 11.10.0 in avanti. Quello che conta sono le impostazioni che erediti su un clone appena fatto, non la marca del package manager.
E Dependabot o Renovate che aprono aggiornamenti ogni giorno?+
Gli aggiornamenti automatici e la disciplina sulle dipendenze tirano in direzioni opposte, se il bot non è configurato per tenerne conto. Raggruppa le pull request, una alla settimana per manifest, dai al bot la stessa finestra di attesa che usi in installazione e chiedi che il delta del lockfile sia scritto nella descrizione. Un bot che apre trenta pull request a settimana viene approvato senza leggere, e così ricrei esattamente il rischio per cui l'avevi messo.
Servizi correlati
Studio
Inizia un progetto.
Scriviamo riguardo a ciò che costruiamo. Raccontaci cosa vuoi costruire tu.