Bonnes pratiques de sécurité
Intention
Ce document vise à étendre le modèle de menaces actuel et à fournir des lignes directrices complètes sur la sécurisation d'une application Node.js.
Contenu du document
- Bonnes pratiques : une manière simplifiée et condensée de voir les meilleures pratiques. Nous pouvons utiliser ce problème ou ces lignes directrices comme point de départ. Il est important de noter que ce document est spécifique à Node.js, si vous recherchez quelque chose de plus large, consultez les bonnes pratiques OSSF.
- Attaques expliquées : illustrer et documenter en termes simples, avec des exemples de code (si possible), les attaques mentionnées dans le modèle de menaces.
- Bibliothèques tierces : définir les menaces (attaques par typosquatting, paquets malveillants…) et les meilleures pratiques concernant les dépendances des modules Node, etc.
Liste des menaces
Déni de service du serveur HTTP (CWE-400)
Il s'agit d'une attaque où l'application devient indisponible pour l'usage auquel elle était destinée en raison de la manière dont elle traite les requêtes HTTP entrantes. Ces requêtes ne doivent pas nécessairement être délibérément conçues par un acteur malveillant : un client mal configuré ou bogué peut également envoyer un motif de requêtes au serveur qui entraîne un déni de service.
Les requêtes HTTP sont reçues par le serveur HTTP Node.js et transmises au code de l'application via le gestionnaire de requêtes enregistré. Le serveur n'analyse pas le contenu du corps de la requête. Par conséquent, tout déni de service causé par le contenu du corps après sa transmission au gestionnaire de requêtes n'est pas une vulnérabilité de Node.js lui-même, car il incombe au code de l'application de le gérer correctement.
Assurez-vous que le serveur Web gère correctement les erreurs de socket, par exemple, lorsqu'un serveur est créé sans gestionnaire d'erreurs, il sera vulnérable aux dénis de service.
import net from 'node:net'
const server = net.createServer(socket => {
// socket.on('error', console.error) // ceci empêche le serveur de planter
socket.write('Echo server\r\n')
socket.pipe(socket)
})
server.listen(5000, '0.0.0.0')
Si une mauvaise requête est effectuée, le serveur pourrait planter.
Un exemple d'attaque par déni de service qui n'est pas causée par le contenu de la requête est Slowloris. Dans cette attaque, les requêtes HTTP sont envoyées lentement et fragmentées, un fragment à la fois. Jusqu'à ce que la requête complète soit livrée, le serveur conservera les ressources dédiées à la requête en cours. Si suffisamment de ces requêtes sont envoyées en même temps, le nombre de connexions simultanées atteindra bientôt son maximum, entraînant un déni de service. C'est ainsi que l'attaque dépend non pas du contenu de la requête, mais du calendrier et du motif des requêtes envoyées au serveur.
Atténuations
- Utilisez un proxy inverse pour recevoir et transférer les requêtes à l'application Node.js. Les proxies inverses peuvent fournir une mise en cache, un équilibrage de charge, une liste noire d'IP, etc., ce qui réduit la probabilité qu'une attaque par déni de service soit efficace.
- Configurez correctement les délais d'expiration du serveur afin que les connexions inactives ou pour lesquelles les requêtes arrivent trop lentement puissent être abandonnées. Consultez les différents délais d'expiration dans
http.Server
, en particulierheadersTimeout
,requestTimeout
,timeout
etkeepAliveTimeout
. - Limitez le nombre de sockets ouverts par hôte et au total. Consultez la documentation http, en particulier
agent.maxSockets
,agent.maxTotalSockets
,agent.maxFreeSockets
etserver.maxRequestsPerSocket
.
Reliaison DNS (CWE-346)
Il s'agit d'une attaque qui peut cibler les applications Node.js exécutées avec l'inspecteur de débogage activé à l'aide de l' option --inspect.
Étant donné que les sites Web ouverts dans un navigateur Web peuvent effectuer des requêtes WebSocket et HTTP, ils peuvent cibler l'inspecteur de débogage s'exécutant localement. Ceci est généralement empêché par la politique de même origine implémentée par les navigateurs modernes, qui interdit aux scripts d'accéder aux ressources provenant d'origines différentes (ce qui signifie qu'un site Web malveillant ne peut pas lire les données demandées à partir d'une adresse IP locale).
Cependant, grâce à la reliaison DNS, un attaquant peut temporairement contrôler l'origine de ses requêtes afin qu'elles semblent provenir d'une adresse IP locale. Ceci est fait en contrôlant à la fois un site Web et le serveur DNS utilisé pour résoudre son adresse IP. Consultez le wiki sur la reliaison DNS pour plus de détails.
Atténuations
- Désactivez l'inspecteur sur le signal SIGUSR1 en y attachant un écouteur
process.on(‘SIGUSR1’, …)
. - N'exécutez pas le protocole de l'inspecteur en production.
Exposition d'informations sensibles à un acteur non autorisé (CWE-552)
Tous les fichiers et dossiers inclus dans le répertoire courant sont poussés vers le registre npm lors de la publication du package.
Il existe certains mécanismes pour contrôler ce comportement en définissant une liste noire avec .npmignore
et .gitignore
ou en définissant une liste blanche dans le package.json
.
Atténuations
- Utiliser
npm publish --dry-run
pour lister tous les fichiers à publier. Assurez-vous de revoir le contenu avant de publier le paquet. - Il est également important de créer et de maintenir des fichiers d'ignore tels que
.gitignore
et.npmignore
. Dans ces fichiers, vous pouvez spécifier les fichiers/dossiers qui ne doivent pas être publiés. La propriété files danspackage.json
permet l'opération inverse, une liste d'éléments autorisés. - En cas d'exposition, assurez-vous de retirer le paquet.
Contrebande de requête HTTP (CWE-444)
Il s'agit d'une attaque impliquant deux serveurs HTTP (généralement un proxy et une application Node.js). Un client envoie une requête HTTP qui passe d'abord par le serveur frontal (le proxy), puis est redirigée vers le serveur principal (l'application). Lorsque le serveur frontal et le serveur principal interprètent différemment les requêtes HTTP ambiguës, il existe un risque qu'un attaquant envoie un message malveillant qui ne sera pas vu par le serveur frontal mais qui sera vu par le serveur principal, le « contrebandant » efficacement par-delà le serveur proxy.
Voir le CWE-444 pour une description et des exemples plus détaillés.
Étant donné que cette attaque dépend de l'interprétation des requêtes HTTP par Node.js différemment d'un serveur HTTP (arbitraire), une attaque réussie peut être due à une vulnérabilité dans Node.js, le serveur frontal, ou les deux. Si la manière dont la requête est interprétée par Node.js est cohérente avec la spécification HTTP (voir RFC7230), alors elle n'est pas considérée comme une vulnérabilité dans Node.js.
Atténuations
- N'utilisez pas l'option
insecureHTTPParser
lors de la création d'un serveur HTTP. - Configurez le serveur frontal pour normaliser les requêtes ambiguës.
- Surveillez en permanence les nouvelles vulnérabilités de contrebande de requête HTTP dans Node.js et le serveur frontal choisi.
- Utilisez HTTP/2 de bout en bout et désactivez la rétrogradation HTTP si possible.
Exposition d'informations via des attaques par analyse temporelle (CWE-208)
Il s'agit d'une attaque qui permet à un attaquant d'obtenir des informations potentiellement sensibles en mesurant, par exemple, le temps de réponse d'une application à une requête. Cette attaque n'est pas spécifique à Node.js et peut cibler presque tous les environnements d'exécution.
L'attaque est possible chaque fois que l'application utilise un secret dans une opération sensible au temps (par exemple, une branche conditionnelle). Considérons la gestion de l'authentification dans une application typique. Une méthode d'authentification basique inclut l'e-mail et le mot de passe comme informations d'identification. Les informations utilisateur sont récupérées à partir des données fournies par l'utilisateur, idéalement depuis une base de données (DBMS). Une fois les informations utilisateur récupérées, le mot de passe est comparé aux informations utilisateur récupérées de la base de données. L'utilisation de la comparaison de chaînes intégrée prend plus de temps pour des valeurs de même longueur. Cette comparaison, lorsqu'elle est exécutée pendant une durée acceptable, augmente involontairement le temps de réponse de la requête. En comparant les temps de réponse des requêtes, un attaquant peut deviner la longueur et la valeur du mot de passe en effectuant un grand nombre de requêtes.
Atténuations
- L'API cryptographique expose une fonction
timingSafeEqual
pour comparer les valeurs sensibles réelles et attendues à l'aide d'un algorithme à temps constant. - Pour la comparaison de mots de passe, vous pouvez utiliser la fonction scrypt également disponible dans le module cryptographique natif.
- Plus généralement, évitez d'utiliser des secrets dans des opérations à temps variable. Cela inclut les branchements conditionnels basés sur des secrets et, lorsque l'attaquant pourrait être co-localisé sur la même infrastructure (par exemple, la même machine cloud), l'utilisation d'un secret comme index en mémoire. Écrire du code à temps constant en JavaScript est difficile (en partie à cause du JIT). Pour les applications cryptographiques, utilisez les API cryptographiques intégrées ou WebAssembly (pour les algorithmes non implémentés nativement).
Modules tiers malveillants (CWE-1357)
Actuellement, dans Node.js, tout package peut accéder à des ressources puissantes telles que l'accès réseau. De plus, comme ils ont également accès au système de fichiers, ils peuvent envoyer toutes les données n'importe où.
Tout code exécuté dans un processus Node.js a la capacité de charger et d'exécuter du code arbitraire supplémentaire en utilisant eval()
(ou ses équivalents). Tout code ayant un accès en écriture au système de fichiers peut faire de même en écrivant dans des fichiers nouveaux ou existants qui sont chargés.
Node.js possède un mécanisme de politique expérimental¹ pour déclarer les ressources chargées comme non fiables ou fiables. Cependant, cette politique n'est pas activée par défaut. Assurez-vous de verrouiller les versions des dépendances et d'exécuter des vérifications automatiques des vulnérabilités en utilisant des workflows courants ou des scripts npm. Avant d'installer un package, assurez-vous qu'il est maintenu et qu'il inclut tout le contenu attendu. Attention, le code source GitHub n'est pas toujours identique à la version publiée, validez-le dans le répertoire node_modules
.
Attaques de la chaîne d'approvisionnement
Une attaque de la chaîne d'approvisionnement sur une application Node.js se produit lorsqu'une de ses dépendances (directes ou transitives) est compromise. Cela peut se produire soit parce que l'application est trop laxiste dans la spécification des dépendances (autorisant des mises à jour non souhaitées) et/ou à cause de fautes de frappe courantes dans la spécification (vulnérable au typosquatting).
Un attaquant qui prend le contrôle d'un paquet en amont peut publier une nouvelle version contenant du code malveillant. Si une application Node.js dépend de ce paquet sans être stricte sur la version à utiliser, le paquet peut être automatiquement mis à jour vers la dernière version malveillante, compromettant ainsi l'application.
Les dépendances spécifiées dans le fichier package.json
peuvent avoir un numéro de version exact ou une plage. Cependant, lorsqu'on fixe une dépendance à une version exacte, ses dépendances transitives ne sont pas elles-mêmes fixées. Cela laisse toujours l'application vulnérable à des mises à jour non souhaitées/inattendues.
Vecteurs d'attaque possibles :
- Attaques par typosquatting
- Intoxication du lockfile
- Maintiens compromis
- Paquets malveillants
- Confusions de dépendances
Atténuations
- Empêcher npm d'exécuter des scripts arbitraires avec
--ignore-scripts
- De plus, vous pouvez le désactiver globalement avec
npm config set ignore-scripts true
- De plus, vous pouvez le désactiver globalement avec
- Fixer les versions des dépendances à une version immuable spécifique, et non à une version qui est une plage ou provenant d'une source mutable.
- Utiliser des lockfiles, qui fixent toutes les dépendances (directes et transitives).
- Automatiser les vérifications des nouvelles vulnérabilités à l'aide de CI, avec des outils comme npm-audit.
- Des outils tels que
Socket
peuvent être utilisés pour analyser les paquets avec une analyse statique afin de trouver des comportements risqués tels que l'accès au réseau ou au système de fichiers.
- Des outils tels que
- Utiliser
npm ci
au lieu denpm install
. Cela impose le lockfile afin que les incohérences entre celui-ci et le fichierpackage.json
provoquent une erreur (au lieu d'ignorer silencieusement le lockfile au profit depackage.json
). - Vérifier attentivement le fichier
package.json
pour détecter les erreurs/fautes de frappe dans les noms des dépendances.
Violation d'accès mémoire (CWE-284)
Les attaques basées sur la mémoire ou le tas dépendent d'une combinaison d'erreurs de gestion de la mémoire et d'un allocateur de mémoire exploitable. Comme tous les environnements d'exécution, Node.js est vulnérable à ces attaques si vos projets s'exécutent sur une machine partagée. L'utilisation d'un tas sécurisé est utile pour empêcher la fuite d'informations sensibles dues aux dépassements et aux sous-dépassements de pointeurs.
Malheureusement, un tas sécurisé n'est pas disponible sous Windows. Plus d'informations peuvent être trouvées dans la documentation Node.js sur le tas sécurisé.
Atténuations
- Utilisez
--secure-heap=n
en fonction de votre application, où n est la taille maximale allouée en octets. - N'exécutez pas votre application de production sur une machine partagée.
Monkey Patching (CWE-349)
Le monkey patching fait référence à la modification des propriétés à l'exécution dans le but de changer le comportement existant. Exemple :
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// remplacement du [].push global
}
Atténuations
L'indicateur --frozen-intrinsics
active les intrinsèques figés expérimentaux¹, ce qui signifie que tous les objets et fonctions JavaScript intégrés sont figés récursivement. Par conséquent, l'extrait suivant ne remplacera pas le comportement par défaut de Array.prototype.push
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// remplacement du [].push global
}
// Erreur non gérée :
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Impossible d'assigner à la propriété en lecture seule 'push' de l'objet '
Cependant, il est important de mentionner que vous pouvez toujours définir de nouvelles variables globales et remplacer les variables globales existantes à l'aide de globalThis
globalThis.foo = 3; foo; // vous pouvez toujours définir de nouvelles variables globales 3
globalThis.Array = 4; Array; // Cependant, vous pouvez également remplacer les variables globales existantes 4
Par conséquent, Object.freeze(globalThis)
peut être utilisé pour garantir qu'aucune variable globale ne sera remplacée.
Attaques par pollution de prototype (CWE-1321)
La pollution de prototype fait référence à la possibilité de modifier ou d'injecter des propriétés dans les éléments du langage Javascript en abusant de l'utilisation de __proto__, _constructor, prototype et d'autres propriétés héritées des prototypes intégrés.
const a = { a: 1, b: 2 }
const data = JSON.parse('{"__proto__": { "polluted": true}}')
const c = Object.assign({}, a, data)
console.log(c.polluted) // true
// Attaque par déni de service potentiel
const data2 = JSON.parse('{"__proto__": null}')
const d = Object.assign(a, data2)
d.hasOwnProperty('b') // TypeError non géré : d.hasOwnProperty n'est pas une fonction
Il s'agit d'une vulnérabilité potentielle héritée du langage JavaScript.
Exemples
- CVE-2022-21824 (Node.js)
- CVE-2018-3721 (bibliothèque tierce : Lodash)
Atténuations
- Évitez les fusions récursives non sécurisées, voir CVE-2018-16487.
- Implémentez des validations JSON Schema pour les requêtes externes/non fiables.
- Créez des objets sans prototype en utilisant
Object.create(null)
. - Congeler le prototype :
Object.freeze(MyObject.prototype)
. - Désactivez la propriété
Object.prototype.__proto__
en utilisant l'indicateur--disable-proto
. - Vérifiez que la propriété existe directement sur l'objet, et non pas à partir du prototype en utilisant
Object.hasOwn(obj, keyFromObj)
. - Évitez d'utiliser les méthodes de
Object.prototype
.
Élément de chemin de recherche non contrôlé (CWE-427)
Node.js charge les modules en suivant l' algorithme de résolution de module. Par conséquent, il suppose que le répertoire dans lequel un module est demandé (require) est fiable.
Cela signifie que le comportement de l'application suivant est attendu. En supposant la structure de répertoire suivante :
- app/
- server.js
- auth.js
- auth
Si server.js utilise require('./auth')
, il suivra l'algorithme de résolution de module et chargera auth au lieu de auth.js
.
Atténuations
L'utilisation du mécanisme expérimental¹ de contrôle des politiques avec vérification d'intégrité peut éviter la menace ci-dessus. Pour le répertoire décrit ci-dessus, vous pouvez utiliser le fichier policy.json
suivant :
{
"resources": {
"./app/auth.js": {
"integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
},
"./app/server.js": {
"dependencies": {
"./auth": "./app/auth.js"
},
"integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
}
}
}
Par conséquent, lors de la demande du module auth, le système validera l'intégrité et générera une erreur si elle ne correspond pas à celle attendue.
» 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]: La chaîne d'intégrité de sous-ressource "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" comportait un "%" inattendu à la 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'
}
Remarque : il est toujours recommandé d'utiliser --policy-integrity
pour éviter les mutations de politique.
Fonctionnalités Expérimentales en Production
L'utilisation de fonctionnalités expérimentales en production n'est pas recommandée. Les fonctionnalités expérimentales peuvent subir des changements importants si nécessaire, et leur fonctionnalité n'est pas stable de manière sécurisée. Cependant, vos retours sont très appréciés.
Outils OpenSSF
L'OpenSSF mène plusieurs initiatives qui peuvent être très utiles, surtout si vous prévoyez de publier un paquet npm. Ces initiatives incluent :
- OpenSSF Scorecard Scorecard évalue les projets open source à l'aide d'une série de vérifications de risques de sécurité automatisées. Vous pouvez l'utiliser pour évaluer proactivement les vulnérabilités et les dépendances dans votre base de code et prendre des décisions éclairées concernant l'acceptation des vulnérabilités.
- Programme de badges OpenSSF Best Practices Les projets peuvent volontairement s'auto-certifier en décrivant comment ils se conforment à chaque meilleure pratique. Cela générera un badge qui peut être ajouté au projet.