Best Practice di Sicurezza
Intenzione
Questo documento intende estendere l'attuale modello di minacce e fornire linee guida esaustive su come proteggere un'applicazione Node.js.
Contenuto del Documento
- Best practice: un modo semplificato e condensato per visualizzare le best practice. Possiamo utilizzare questa issue o questa guida come punto di partenza. È importante notare che questo documento è specifico per Node.js, se si cerca qualcosa di più ampio, considerare le Best Practice OSSF.
- Attaccchi spiegati: illustrare e documentare in linguaggio semplice con alcuni esempi di codice (se possibile) gli attacchi che vengono menzionati nel modello di minacce.
- Librerie di terze parti: definire le minacce (attacchi di typosquatting, pacchetti dannosi...) e le best practice riguardanti le dipendenze dei moduli node, ecc...
Elenco delle Minacce
Denial of Service del server HTTP (CWE-400)
Questo è un attacco in cui l'applicazione diventa indisponibile per lo scopo per cui è stata progettata a causa del modo in cui elabora le richieste HTTP in arrivo. Queste richieste non devono essere necessariamente create deliberatamente da un attore malintenzionato: un client mal configurato o con bug può anche inviare un modello di richieste al server che si traduce in un denial of service.
Le richieste HTTP vengono ricevute dal server HTTP Node.js e consegnate al codice dell'applicazione tramite il gestore di richieste registrato. Il server non analizza il contenuto del corpo della richiesta. Pertanto, qualsiasi DoS causato dal contenuto del corpo dopo che sono stati consegnati al gestore di richieste non è una vulnerabilità di Node.js stesso, poiché è responsabilità del codice dell'applicazione gestirlo correttamente.
Assicurarsi che il WebServer gestisca correttamente gli errori del socket, ad esempio, quando un server viene creato senza un gestore di errori, sarà vulnerabile a DoS.
import net from 'node:net'
const server = net.createServer(socket => {
// socket.on('error', console.error) // questo impedisce al server di crashare
socket.write('Echo server\r\n')
socket.pipe(socket)
})
server.listen(5000, '0.0.0.0')
Se viene eseguita una richiesta errata, il server potrebbe bloccarsi.
Un esempio di attacco DoS che non è causato dal contenuto della richiesta è Slowloris. In questo attacco, le richieste HTTP vengono inviate lentamente e frammentate, un frammento alla volta. Fino a quando non viene consegnata la richiesta completa, il server manterrà le risorse dedicate alla richiesta in corso. Se vengono inviate contemporaneamente abbastanza di queste richieste, la quantità di connessioni concorrenti raggiungerà presto il suo massimo, causando un denial of service. Questo è il modo in cui l'attacco dipende non dal contenuto della richiesta, ma dalla tempistica e dal modello delle richieste inviate al server.
Mitigazioni
- Utilizzare un reverse proxy per ricevere e inoltrare le richieste all'applicazione Node.js. I reverse proxy possono fornire caching, bilanciamento del carico, blacklist IP, ecc., riducendo la probabilità che un attacco DoS sia efficace.
- Configurare correttamente i timeout del server, in modo che le connessioni inattive o in cui le richieste arrivano troppo lentamente possano essere interrotte. Vedere i diversi timeout in
http.Server
, in particolareheadersTimeout
,requestTimeout
,timeout
ekeepAliveTimeout
. - Limitare il numero di socket aperti per host e in totale. Vedere la documentazione http, in particolare
agent.maxSockets
,agent.maxTotalSockets
,agent.maxFreeSockets
eserver.maxRequestsPerSocket
.
Rebinding DNS (CWE-346)
Questo è un attacco che può colpire le applicazioni Node.js eseguite con l'inspector di debug abilitato usando lo switch --inspect.
Poiché i siti web aperti in un browser web possono effettuare richieste WebSocket e HTTP, possono prendere di mira l'inspector di debug in esecuzione localmente. Questo è generalmente impedito dalla policy dell'origine stessa implementata dai browser moderni, che proibisce agli script di raggiungere risorse da origini diverse (il che significa che un sito web dannoso non può leggere i dati richiesti da un indirizzo IP locale).
Tuttavia, tramite il rebinding DNS, un attaccante può temporaneamente controllare l'origine delle proprie richieste in modo che sembrino provenire da un indirizzo IP locale. Questo viene fatto controllando sia un sito web che il server DNS utilizzato per risolvere il suo indirizzo IP. Vedere la wiki sul Rebinding DNS per maggiori dettagli.
Mitigazioni
- Disabilitare l'inspector sul segnale SIGUSR1 attaccandovi un listener
process.on(‘SIGUSR1’, …)
- Non eseguire il protocollo inspector in produzione.
Esposizione di informazioni sensibili a un attore non autorizzato (CWE-552)
Tutti i file e le cartelle inclusi nella directory corrente vengono inviati al registro npm durante la pubblicazione del pacchetto.
Esistono alcuni meccanismi per controllare questo comportamento definendo una blacklist con .npmignore
e .gitignore
o definendo una whitelist in package.json
Mitigazioni
- Utilizzo di
npm publish --dry-run
per elencare tutti i file da pubblicare. Assicurarsi di rivedere il contenuto prima di pubblicare il pacchetto. - È inoltre importante creare e mantenere file di ignore come
.gitignore
e.npmignore
. In questi file, è possibile specificare quali file/cartelle non devono essere pubblicati. La proprietàfiles
inpackage.json
consente l'operazione inversa di lista-- allowed
. - In caso di esposizione, assicurarsi di rimuovere il pacchetto dal registro.
Smuggling di richieste HTTP (CWE-444)
Questo è un attacco che coinvolge due server HTTP (solitamente un proxy e un'applicazione Node.js). Un client invia una richiesta HTTP che passa prima attraverso il server front-end (il proxy) e poi viene reindirizzata al server back-end (l'applicazione). Quando il front-end e il back-end interpretano le richieste HTTP ambigue in modo diverso, esiste la possibilità che un attaccante invii un messaggio dannoso che non verrà visto dal front-end ma verrà visto dal back-end, "contrabbandandolo" efficacemente oltre il server proxy.
Vedere CWE-444 per una descrizione più dettagliata ed esempi.
Poiché questo attacco dipende dall'interpretazione delle richieste HTTP da parte di Node.js in modo diverso da un server HTTP (arbitrario), un attacco riuscito può essere dovuto a una vulnerabilità in Node.js, nel server front-end, o in entrambi. Se il modo in cui la richiesta viene interpretata da Node.js è coerente con le specifiche HTTP (vedere RFC7230), allora non è considerata una vulnerabilità in Node.js.
Mitigazioni
- Non utilizzare l'opzione
insecureHTTPParser
durante la creazione di un server HTTP. - Configurare il server front-end per normalizzare le richieste ambigue.
- Monitorare continuamente le nuove vulnerabilità di smuggling delle richieste HTTP sia in Node.js che nel server front-end scelto.
- Utilizzare HTTP/2 end-to-end e disabilitare il downgrade HTTP, se possibile.
Esposizione di informazioni tramite attacchi temporali (CWE-208)
Questo è un attacco che consente all'attaccante di apprendere informazioni potenzialmente sensibili misurando, ad esempio, il tempo impiegato dall'applicazione per rispondere a una richiesta. Questo attacco non è specifico di Node.js e può colpire quasi tutti i runtime.
L'attacco è possibile ogni volta che l'applicazione utilizza un segreto in un'operazione sensibile al tempo (es., ramo). Si consideri la gestione dell'autenticazione in un'applicazione tipica. Qui, un metodo di autenticazione di base include email e password come credenziali. Le informazioni utente vengono recuperate dall'input fornito dall'utente, idealmente da un DBMS. Dopo aver recuperato le informazioni dell'utente, la password viene confrontata con le informazioni dell'utente recuperate dal database. L'utilizzo del confronto di stringhe integrato richiede più tempo per valori della stessa lunghezza. Questo confronto, quando eseguito per una quantità accettabile, aumenta involontariamente il tempo di risposta della richiesta. Confrontando i tempi di risposta delle richieste, un attaccante può indovinare la lunghezza e il valore della password in un gran numero di richieste.
Soluzioni
- L'API crittografica espone una funzione
timingSafeEqual
per confrontare i valori sensibili effettivi e previsti utilizzando un algoritmo a tempo costante. - Per il confronto delle password, è possibile utilizzare lo strumento scrypt disponibile anche nel modulo crypto nativo.
- Più in generale, evitare di utilizzare segreti in operazioni a tempo variabile. Ciò include la ramificazione su segreti e, quando l'attaccante potrebbe essere co-locato sulla stessa infrastruttura (es., stessa macchina cloud), l'utilizzo di un segreto come indice nella memoria. Scrivere codice a tempo costante in JavaScript è difficile (in parte a causa del JIT). Per le applicazioni crittografiche, utilizzare le API crittografiche integrate o WebAssembly (per gli algoritmi non implementati nativamente).
Moduli di terze parti dannosi (CWE-1357)
Attualmente, in Node.js, qualsiasi pacchetto può accedere a risorse potenti come l'accesso alla rete. Inoltre, poiché hanno anche accesso al file system, possono inviare qualsiasi dato ovunque.
Tutto il codice in esecuzione in un processo node ha la capacità di caricare ed eseguire codice arbitrario aggiuntivo utilizzando eval()
(o i suoi equivalenti). Tutto il codice con accesso in scrittura al file system può ottenere lo stesso risultato scrivendo su file nuovi o esistenti che vengono caricati.
Node.js ha un meccanismo di policy sperimentale¹ per dichiarare la risorsa caricata come non attendibile o attendibile. Tuttavia, questa policy non è abilitata per impostazione predefinita. Assicurarsi di bloccare le versioni delle dipendenze ed eseguire controlli automatici per le vulnerabilità utilizzando flussi di lavoro comuni o script npm. Prima di installare un pacchetto, assicurarsi che questo pacchetto sia mantenuto e includa tutto il contenuto previsto. Attenzione, il codice sorgente GitHub non è sempre uguale a quello pubblicato, convalidarlo in node_modules
.
Attacchi alla catena di approvvigionamento
Un attacco alla catena di approvvigionamento a un'applicazione Node.js si verifica quando una delle sue dipendenze (dirette o transitive) viene compromessa. Ciò può accadere a causa di una specifica delle dipendenze troppo lasca da parte dell'applicazione (che consente aggiornamenti indesiderati) e/o errori di battitura comuni nella specifica (vulnerabile allo typosquatting).
Un attaccante che prende il controllo di un pacchetto upstream può pubblicare una nuova versione con codice dannoso al suo interno. Se un'applicazione Node.js dipende da quel pacchetto senza essere rigorosa su quale versione è sicura da utilizzare, il pacchetto può essere aggiornato automaticamente all'ultima versione dannosa, compromettendo l'applicazione.
Le dipendenze specificate nel file package.json
possono avere un numero di versione esatto o un intervallo. Tuttavia, quando si fissa una dipendenza a una versione esatta, le sue dipendenze transitive non sono a loro volta fissate. Ciò lascia comunque l'applicazione vulnerabile ad aggiornamenti indesiderati/inaspettati.
Possibili vettori di attacco:
- Attacchi di typosquatting
- Avvelenamento del lockfile
- Manutentori compromessi
- Pacchetti dannosi
- Confusione delle dipendenze
Mitigazioni
- Impedire a npm di eseguire script arbitrari con
--ignore-scripts
- Inoltre, puoi disabilitarlo globalmente con
npm config set ignore-scripts true
- Inoltre, puoi disabilitarlo globalmente con
- Fissare le versioni delle dipendenze a una versione immutabile specifica, non a una versione che è un intervallo o da una sorgente mutabile.
- Utilizzare i lockfile, che fissano ogni dipendenza (diretta e transitiva).
- Utilizzare Mitigazioni per l'avvelenamento del lockfile.
- Automatizzare i controlli per nuove vulnerabilità utilizzando CI, con strumenti come npm-audit.
- Strumenti come
Socket
possono essere utilizzati per analizzare i pacchetti con analisi statica per trovare comportamenti rischiosi come l'accesso alla rete o al filesystem.
- Strumenti come
- Utilizzare
npm ci
invece dinpm install
. Ciò impone il lockfile in modo che le incoerenze tra esso e il filepackage.json
causino un errore (invece di ignorare silenziosamente il lockfile a favore dipackage.json
). - Controllare attentamente il file
package.json
per errori/errori di battitura nei nomi delle dipendenze.
Violazione dell'accesso alla memoria (CWE-284)
Gli attacchi basati sulla memoria o sull'heap dipendono da una combinazione di errori di gestione della memoria e da un allocatore di memoria sfruttabile. Come tutti i runtime, Node.js è vulnerabile a questi attacchi se i vostri progetti vengono eseguiti su una macchina condivisa. L'utilizzo di un heap sicuro è utile per impedire la perdita di informazioni sensibili a causa di overrun e underrun di puntatori.
Sfortunatamente, un heap sicuro non è disponibile su Windows. Ulteriori informazioni possono essere trovate nella documentazione di Node.js sull'heap sicuro.
Soluzioni
- Utilizzare
--secure-heap=n
a seconda della vostra applicazione, dove n è la dimensione massima allocata in byte. - Non eseguire l'applicazione di produzione su una macchina condivisa.
Monkey Patching (CWE-349)
Il monkey patching si riferisce alla modifica delle proprietà in fase di runtime con l'obiettivo di cambiare il comportamento esistente. Esempio:
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// sovrascrittura del globale [].push
}
Soluzioni
Il flag --frozen-intrinsics
abilita gli intrinsics congelati sperimentali¹, il che significa che tutti gli oggetti e le funzioni JavaScript built-in sono congelati ricorsivamente. Pertanto, il seguente snippet non sovrascriverà il comportamento predefinito di Array.prototype.push
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// sovrascrittura del globale [].push
}
// Eccezione non gestita:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Impossibile assegnare alla proprietà di sola lettura 'push' dell'oggetto '
Tuttavia, è importante menzionare che è comunque possibile definire nuovi globali e sostituire quelli esistenti usando globalThis
globalThis.foo = 3; foo; // è comunque possibile definire nuovi globali 3
globalThis.Array = 4; Array; // Tuttavia, è possibile anche sostituire i globali esistenti 4
Pertanto, Object.freeze(globalThis)
può essere utilizzato per garantire che nessun globale venga sostituito.
Attacchi di inquinamento del prototipo (CWE-1321)
L'inquinamento del prototipo si riferisce alla possibilità di modificare o iniettare proprietà negli elementi del linguaggio Javascript abusando dell'utilizzo di __proto__, _constructor, prototype e altre proprietà ereditate dai prototipi built-in.
const a = { a: 1, b: 2 }
const data = JSON.parse('{"__proto__": { "polluted": true}}')
const c = Object.assign({}, a, data)
console.log(c.polluted) // true
// Potenziale DoS
const data2 = JSON.parse('{"__proto__": null}')
const d = Object.assign(a, data2)
d.hasOwnProperty('b') // Eccezione TypeError non gestita: d.hasOwnProperty non è una funzione
Questa è una potenziale vulnerabilità ereditata dal linguaggio JavaScript.
Esempi
- CVE-2022-21824 (Node.js)
- CVE-2018-3721 (Libreria di terze parti: Lodash)
Soluzioni
- Evitare le fusioni ricorsive non sicure, vedi CVE-2018-16487.
- Implementare le convalide JSON Schema per le richieste esterne/non attendibili.
- Creare oggetti senza prototipo usando
Object.create(null)
. - Congelare il prototipo:
Object.freeze(MyObject.prototype)
. - Disabilitare la proprietà
Object.prototype.__proto__
usando il flag--disable-proto
. - Verificare che la proprietà esista direttamente sull'oggetto, non dal prototipo usando
Object.hasOwn(obj, keyFromObj)
. - Evitare l'utilizzo di metodi da
Object.prototype
.
Elemento di percorso di ricerca non controllato (CWE-427)
Node.js carica i moduli seguendo l'algoritmo di risoluzione dei moduli. Pertanto, presuppone che la directory in cui viene richiesto un modulo (require) sia attendibile.
Ciò significa che il seguente comportamento dell'applicazione è previsto. Supponendo la seguente struttura di directory:
- app/
- server.js
- auth.js
- auth
Se server.js usa require('./auth')
seguirà l'algoritmo di risoluzione dei moduli e caricherà auth invece di auth.js
.
Soluzioni
L'utilizzo del meccanismo di policy con controllo di integrità sperimentale¹ può evitare la minaccia di cui sopra. Per la directory descritta sopra, è possibile utilizzare il seguente policy.json
{
"resources": {
"./app/auth.js": {
"integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
},
"./app/server.js": {
"dependencies": {
"./auth": "./app/auth.js"
},
"integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
}
}
}
Pertanto, quando si richiede il modulo auth, il sistema convaliderebbe l'integrità e solleverebbe un errore se non corrisponde a quello previsto.
» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
^
SyntaxError [ERR_SRI_PARSE]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 51
at new NodeError (node:internal/errors:393:5)
at Object.parse (node:internal/policy/sri:65:13)
at processEntry (node:internal/policy/manifest:581:38)
at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
at Module._compile (node:internal/modules/cjs/loader:1119:21)
at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Module._load (node:internal/modules/cjs/loader:878:12)
at Module.require (node:internal/modules/cjs/loader:1061:19)
at require (node:internal/modules/cjs/helpers:99:18) {
code: 'ERR_SRI_PARSE'
}
Si consiglia sempre l'utilizzo di --policy-integrity
per evitare mutazioni delle policy.
Funzionalità Sperimentali in Produzione
L'utilizzo di funzionalità sperimentali in produzione non è raccomandato. Le funzionalità sperimentali possono subire modifiche sostanziali se necessario, e la loro funzionalità non è stabilmente sicura. Tuttavia, il feedback è molto apprezzato.
Strumenti OpenSSF
L'OpenSSF sta guidando diverse iniziative che possono essere molto utili, soprattutto se si prevede di pubblicare un pacchetto npm. Queste iniziative includono:
- OpenSSF Scorecard Scorecard valuta i progetti open source utilizzando una serie di controlli automatici del rischio di sicurezza. È possibile utilizzarlo per valutare proattivamente vulnerabilità e dipendenze nel proprio codice sorgente e prendere decisioni informate sull'accettazione delle vulnerabilità.
- Programma Badge OpenSSF Best Practices I progetti possono autocertificarsi volontariamente descrivendo come rispettano ogni best practice. Questo genererà un badge che può essere aggiunto al progetto.