Sicurezza delle dipendenze npm: cosa controllare
Sicurezza delle dipendenze npm senza allarmismo: dipendenze transitive, limiti di npm audit, script postinstall, typosquatting e pacchetti abbandonati.
Lanci npm audit su un progetto appena creato e ti dice 42 vulnerabilità, di cui 7 critiche. Il primo istinto è il panico, il secondo è npm audit fix --force, e il terzo — dopo che la build si è rotta — è ignorare del tutto l'argomento. Nessuna delle tre è la reazione giusta. In questo articolo trovi dove sta il rischio vero nelle dipendenze npm, cosa npm audit non ti dice, gli attacchi che funzionano davvero e un elenco di controlli che vale la pena fare prima di installare qualcosa.
Cos'è il rischio della catena di fornitura
Il rischio della catena di fornitura è questo: ogni pacchetto che installi è codice scritto da sconosciuti che eseguirai con i tuoi permessi, e non sono solo quelli che hai scelto tu.
Installi cinque pacchetti. Ognuno ne porta altri, che ne portano altri: finisci con ottocento cartelle in node_modules, scritte da qualche centinaio di persone diverse. La superficie di rischio è quella, non i cinque nomi nel tuo package.json.
Verificalo tu stesso:
npm ls --all | wc -l # quante voci nell'albero completo
npm ls --omit=dev --all # solo ciò che finisce in produzione
Il numero sorprende quasi sempre, ed è il dato da tenere in testa: non controlli quello che installi, controlli solo il punto d'ingresso. Non è un motivo per smettere di usare npm, ma per decidere cosa far entrare invece di aggiungere pacchetti per riflesso.
npm audit, e i suoi limiti
npm audit # tutte le vulnerabilità note
npm audit --omit=dev # solo quelle di produzione
npm audit fix # aggiorna dove si può senza rompere
Funziona confrontando il tuo albero con un database di vulnerabilità pubbliche. È utile e va fatto. Ma va letto sapendo tre cose, altrimenti fa perdere più tempo di quanto ne faccia risparmiare.
Molte segnalazioni non ti riguardano. Una vulnerabilità viene segnalata sul pacchetto, non sul tuo uso del pacchetto. Se la falla sta in una funzione che il tuo codice non chiama mai, tecnicamente sei vulnerabile e praticamente no. Lo strumento non ha modo di distinguere, e quindi ti segnala tutto.
Il caso più frequente è la dipendenza di sviluppo. Una vulnerabilità in un pacchetto usato dal tuo bundler durante la build ha un impatto vicino allo zero: gira sulla tua macchina, su input che scrivi tu, e non finisce nel prodotto. Vederla marcata "critica" è normale, perché la gravità descrive la falla in astratto, non il tuo scenario.
La domanda giusta non è "quante ne ho", ma tre in fila: il pacchetto finisce in produzione? Il codice vulnerabile viene eseguito nel mio flusso? Un attaccante può raggiungerlo con dati che controlla?
Se la risposta è no a tutte e tre, quella segnalazione è rumore. Dirlo apertamente conta, perché il panico da audit porta a due comportamenti peggiori del problema: aggiornare in blocco rompendo la build, o smettere del tutto di guardare i risultati.
npm audit fix --force va evitato. Aggiorna a versioni major con cambiamenti incompatibili pur di azzerare il conteggio. Se lo usi, fallo su un branch con i test che girano.
E un limite che si dimentica: audit vede solo le vulnerabilità già scoperte e pubblicate. Un pacchetto compromesso ieri non è nel database. Contro quello, l'audit non fa nulla.
Gli script postinstall: il vettore vero
Questo è il punto dove il rischio smette di essere teorico.
Un pacchetto può dichiarare script che il gestore esegue automaticamente durante l'installazione:
{
"name": "utility-innocua",
"scripts": {
"postinstall": "node ./setup.js"
}
}
Quel comando gira sulla tua macchina, con i tuoi permessi, senza chiederti nulla. Non serve che tu importi il pacchetto nel codice: basta averlo installato. E l'installazione la fa anche la tua pipeline di CI, dove spesso ci sono variabili d'ambiente con token di deploy e chiavi del registro.
È il meccanismo dietro quasi tutti gli incidenti noti della catena di fornitura npm: un manutentore perde il controllo dell'account, esce una versione con un postinstall aggiunto, e in poche ore quel codice ha girato su migliaia di macchine e pipeline.
La difesa è disattivarli:
npm ci --ignore-scripts
Oppure, in modo permanente, in un file .npmrc alla radice del progetto:
ignore-scripts=true
L'avvertenza onesta: alcuni pacchetti ne hanno bisogno davvero — quelli che compilano codice nativo o scaricano un binario. Con ignore-scripts attivo si installano e poi non funzionano, spesso con errori che non menzionano gli script.
Il compromesso che regge: attivalo in CI, dove l'ambiente è il più prezioso, e in locale usalo sapendo che per una manciata di pacchetti dovrai eseguire la build a mano.
Typosquatting
Un carattere sbagliato e installi un pacchetto diverso. crossenv invece di cross-env, lodahs invece di lodash, un trattino di troppo, una lettera invertita. Sono pacchetti pubblicati apposta con nomi somiglianti, spesso funzionanti — fanno quello che promettono — più un postinstall che raccoglie quello che trova.
Dove il rischio è più alto:
- Copiare comandi da risposte trovate online, incluse quelle generate da un modello linguistico, che a volte inventa nomi di pacchetti plausibili e inesistenti. Qualcuno poi quei nomi li registra.
npx, che scarica ed esegue senza lasciare traccia nelpackage.json. Con il nome sbagliato hai eseguito codice arbitrario e non hai nemmeno un file che lo documenti.- I pacchetti con ambito.
@types/nodeè ufficiale,types-nodeno. L'ambito è un segnale, ma va letto.
La difesa è banale e funziona: guarda il nome prima di premere invio, e installa partendo dalla pagina del pacchetto o dal suo repository, non da una stringa copiata. Il lockfile aiuta dopo, perché registra l'URL esatto e l'impronta: se qualcosa cambia sotto i piedi te ne accorgi. Il meccanismo è in cos'è il lockfile.
I pacchetti abbandonati
Il rischio più sottovalutato, perché non produce nessun avviso. Una libreria non mantenuta non è vulnerabile oggi: semplicemente, il giorno in cui salterà fuori un problema non ci sarà nessuno a correggerlo. E un pacchetto popolare ma trascurato è un bersaglio ideale — sono già successi casi di manutentori stanchi che hanno ceduto il controllo a uno sconosciuto volenteroso.
Come accorgersene, in due minuti:
| Segnale | Dove si guarda | Cosa allarma |
|---|---|---|
| Ultima pubblicazione | pagina npm del pacchetto | oltre due anni senza rilasci |
| Problemi aperti | repository | molti, vecchi, senza risposta |
| Chi mantiene | campo maintainers, cronologia commit | una persona sola |
| Numero di dipendenze | npm ls sul pacchetto | decine, per fare poco |
| Richieste di modifica | repository | ferme da mesi |
| Alternative | ecosistema | esiste un successore consigliato |
npm view nome-pacchetto time.modified # data dell'ultima pubblicazione
npm view nome-pacchetto maintainers # chi ha i permessi di pubblicare
npm view nome-pacchetto dependencies # cosa si porta dietro
Il manutentore singolo non è un difetto, ed è la condizione normale di gran parte dell'ecosistema. È un fatto da mettere in conto: un solo account da compromettere e una sola persona che può smettere quando vuole.
Blocco delle versioni e aggiornamenti automatici
Sembrano opposti e invece sono la stessa strategia.
Blocchi perché non vuoi che una dipendenza cambi da sola tra un giorno e l'altro: il lockfile committato fa esattamente questo, e in produzione si installa con npm ci.
Aggiorni con regolarità perché restare fermi non è sicuro: accumuli vulnerabilità note e dopo due anni ti trovi davanti a un salto di versione impossibile da fare in un pomeriggio.
Gli strumenti che uniscono le due cose sono Dependabot (integrato in GitHub) e Renovate: aprono una pull request per ogni aggiornamento, la CI gira, tu unisci se passa.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
dev-dependencies:
dependency-type: "development"
Il consiglio pratico: raggruppa gli aggiornamenti di sviluppo in un'unica pull request — non serve una richiesta separata per ogni patch del linter — e tieni separati quelli di produzione. Cadenza settimanale: quotidiana produce rumore che smetterai di leggere dopo un mese.
E la condizione che rende sensato tutto questo: serve una suite di test decente. Senza, unire un aggiornamento automatico significa solo spostare il rischio dall'essere fermi all'aggiornare alla cieca. Il tema è in cosa sono i test software e in cos'è la CI/CD.
Cosa fa perdere ore
- Trattare ogni segnalazione come un'emergenza. Valuta se la dipendenza finisce in produzione e se il percorso vulnerabile viene eseguito: spesso la risposta corretta è "lo tengo d'occhio".
npm audit fix --forcesul branch principale. Aggiornamenti major mascherati da correzione di sicurezza.- Aggiungere una dipendenza per tre righe di codice. Ogni pacchetto porta il suo albero, i suoi script e il suo manutentore.
- Installare globalmente per comodità. Un pacchetto globale non ha lockfile, non è nel repository e nessuno sa che c'è.
- Non distinguere sviluppo e produzione.
npm audit --omit=devcambia il quadro e ti dice quale parte del rumore è davvero rumore. - Fidarsi dei numeri di download. Si gonfiano facilmente, e un pacchetto molto scaricato compromesso fa più danni, non meno.
La regola di fondo
Tutto quanto sopra sono mitigazioni. La cosa che sposta davvero l'ago è un'altra, ed è poco entusiasmante: la difesa migliore è avere meno dipendenze, non più strumenti di scansione.
Ogni pacchetto che non installi è codice che non devi verificare, un albero transitivo che non entra, uno script di installazione che non gira, un manutentore di cui non devi fidarti. Nessun audit, nessun bot di aggiornamento e nessuna politica aziendale ti dà lo stesso risultato.
In pratica, tre domande prima di ogni npm install:
- Lo fa già qualcosa che ho? La libreria standard di Node e le API del browser coprono oggi molto più di quanto coprissero quando sono nate certe abitudini.
- Quante righe risparmio davvero? Se sono meno di cinquanta, scriverle è quasi sempre l'opzione migliore.
- Quanto si porta dietro? Controllalo prima di installare, non dopo.
È la stessa logica per cui il codice più affidabile è quello che non scrivi. Il quadro generale è in cos'è la cybersecurity.
In sintesi
Il rischio delle dipendenze non sta nei pacchetti che scegli, ma nelle centinaia che arrivano insieme a loro.
npm audit è utile ma rumoroso: una vulnerabilità in uno strumento di sviluppo che non finisce in produzione non è un'emergenza, e trattarla come tale porta ad aggiornamenti alla cieca o a smettere di guardare del tutto.
Il vettore d'attacco vero sono gli script postinstall, che girano con i tuoi permessi appena installi. --ignore-scripts è la difesa, da attivare almeno in CI.
I pacchetti abbandonati non generano avvisi: controlla data dell'ultima pubblicazione, problemi aperti e quante persone li mantengono prima di aggiungerli.
Blocca con il lockfile e aggiorna con regolarità tramite Dependabot o Renovate, appoggiandoti a test che ti dicano se qualcosa si è rotto.
E la regola che conta più di tutte: meno dipendenze installi, meno superficie hai da difendere.
Per il file che congela l'albero e ne verifica l'integrità, vedi cos'è il lockfile. Per i campi che decidono cosa finisce in produzione, vedi package.json: ogni campo spiegato. Per la sicurezza lato applicazione, vedi rendere un sito sicuro.