Aikido

Pourquoi les variables globales provoquent-elles des fuites de données dans les serveurs Node.js ?

Sécurité

Règle
Éviter involontaire globale variable caching.In Node.js
et Python serveurs, variables variables persistent à travers
requêtes, ce qui fait que les données fuites et conditions de course.

Langages pris en charge : JavaScript, TypeScript, Python

Introduction

Les variables globales dans les serveurs Node.js persistent pendant toute la durée de vie du processus, et pas seulement pour une seule requête. Lorsque les gestionnaires de requêtes stockent des données utilisateur dans des variables globales, ces données restent accessibles aux requêtes ultérieures émanant d'utilisateurs différents. Cela crée des failles de sécurité lorsque les données de session, les jetons d'authentification ou les informations personnelles de l'utilisateur A sont divulguées à l'utilisateur B.

Pourquoi c'est important

Implications en matière de sécurité (fuites de données) : Les variables globales qui mettent en cache des données spécifiques à l'utilisateur créent des fuites de données entre les requêtes. L'état d'authentification, les données de session ou les informations personnelles d'un utilisateur deviennent visibles pour les autres utilisateurs, ce qui constitue une violation de la vie privée et des limites de sécurité.

Conditions de course : Lorsque plusieurs requêtes concurrentes modifient la même variable globale, le risque d'un comportement imprévisible est élevé. Les données de l'utilisateur A peuvent être écrasées par la requête de l'utilisateur B en cours de traitement, ce qui entraîne des calculs incorrects, un état corrompu ou des utilisateurs qui voient les données de l'autre.

Complexité du débogage : les problèmes causés par la mise en cache de variables globales sont notoirement difficiles à reproduire parce qu'ils dépendent de la synchronisation des requêtes et de la concurrence. Les bogues apparaissent par intermittence en production sous charge, mais se manifestent rarement dans les tests de développement à un seul fil.

Fuites de mémoire : Les variables globales qui accumulent des données sans être nettoyées croissent sans limite au fil du temps. Chaque requête ajoute davantage de données aux caches ou aux tableaux globaux, ce qui finit par épuiser la mémoire du serveur et nécessite le redémarrage du processus.

Exemples de code

❌ Non conforme :

let currentUser = null;
let requestData = {};

app.get('/profile', async (req, res) => {
    currentUser = await getUserById(req.userId);
    requestData = req.body;

    const profile = await buildUserProfile(currentUser);
    res.json(profile);
});

function buildUserProfile(user) {
    return {
        name: currentUser.name,
        data: requestData
    };
}

Pourquoi ce n'est pas correct : Les variables globales currentUser et requestData persistent à travers les requêtes. Lorsque plusieurs requêtes s'exécutent simultanément, la requête de l'utilisateur B peut écraser currentUser alors que la fonction buildUserProfile() de l'utilisateur A est toujours en cours d'exécution, ce qui permet à l'utilisateur A de voir les données de l'utilisateur B.

✅ Conforme :

app.get('/profile', async (req, res) => {
    const currentUser = await getUserById(req.userId);
    const requestData = req.body;

    const profile = buildUserProfile(currentUser, requestData);
    res.json(profile);
});

function buildUserProfile(user, data) {
    return {
        name: user.name,
        data: data
    };
}

Pourquoi cela est-il important ? Toutes les données spécifiques à une demande sont stockées dans des variables locales liées au gestionnaire de la demande. Chaque demande a un état isolé qui ne peut pas être divulgué à d'autres demandes concurrentes. Les fonctions reçoivent des données par le biais de paramètres plutôt que d'accéder à l'état global, ce qui élimine les conditions de course.

Conclusion

Conservez toutes les données spécifiques aux requêtes dans des variables locales ou des objets de requête fournis par votre framework. N'utilisez les variables globales que pour les états réellement partagés tels que la configuration, les pools de connexion ou les caches en lecture seule. Lorsque l'état global est nécessaire, utilisez des contrôles de concurrence appropriés et assurez-vous que les données ne sont jamais spécifiques à l'utilisateur.

FAQ

Vous avez des questions ?

Quand peut-on utiliser des variables globales en Node.js ?

Les variables globales sont sûres pour les données en lecture seule qui s'appliquent à toutes les requêtes : configuration de l'application, pools de connexion à la base de données, modèles compilés ou utilitaires partagés. Ne stockez jamais de données spécifiques à une requête ou à un utilisateur au niveau mondial. Si vous devez mettre des données en cache au niveau mondial, assurez-vous qu'elles sont correctement codées et que leur accès est sans risque pour les threads, ou utilisez des solutions de mise en cache appropriées telles que Redis.

Qu'en est-il des variables au niveau du module qui ne sont pas explicitement globales ?

Les variables au niveau du module (const, let, var au niveau du fichier) se comportent exactement comme les variables globales dans Node.js. Elles persistent à travers toutes les requêtes et sont partagées par tous les gestionnaires de requêtes simultanés. Les mêmes risques de fuite de données et de conditions de course s'appliquent. Traitez les variables de niveau module avec la même prudence que les variables globales explicites.

Comment partager des données entre l'intergiciel et les gestionnaires d'itinéraires ?

Utilisez les propriétés de l'objet de requête fournies par votre framework. Express fournit req.locals ou des propriétés personnalisées sur req. Fastify propose request.decorateRequest(). Ces objets sont à l'échelle de la requête et sont automatiquement nettoyés une fois la requête terminée, ce qui évite les fuites entre les requêtes.

Qu'en est-il des modèles singleton et des instances de classe ?

Les instances singleton au niveau du module sont des états globaux. Si elles contiennent des données spécifiques à une requête, les mêmes problèmes se posent. Concevez les singletons de manière à ce qu'ils soient sans état ou qu'ils ne contiennent que la configuration. Pour les opérations avec état, créez de nouvelles instances par demande ou utilisez des modèles d'usine qui garantissent l'isolation.

Comment détecter ces problèmes au cours du développement ?

Exécutez des tests de charge avec des requêtes simultanées en utilisant différents contextes d'utilisateurs. Les conditions de course et les fuites de données n'apparaissent souvent pas lors des tests séquentiels. Utilisez des outils comme Apache Bench ou autocannon pour générer des charges concurrentes. Ajoutez une journalisation qui inclut les identifiants des requêtes afin de savoir quand les données d'une requête apparaissent dans une autre.

Cela s'applique-t-il aux fonctions sans serveur comme AWS Lambda ?

Partiellement. Chaque invocation Lambda obtient un nouvel environnement d'exécution, mais le conteneur peut être réutilisé entre les invocations. Les variables globales persistent entre les invocations qui réutilisent le même conteneur. Ne comptez pas sur la réinitialisation des variables globales. Suivez les mêmes pratiques : gardez les données de la requête dans la portée locale.

Qu'en est-il des applications Python WSGI/ASGI ?

Les mêmes principes s'appliquent. Les serveurs web Python fonctionnent en mode multithread ou asynchrone, de sorte que les variables au niveau du module sont partagées entre les requêtes. L'objet g de Flask et l'injection de dépendances de FastAPI fournissent un stockage à l'échelle de la requête. Django possède des objets de requête. Utilisez les mécanismes fournis par le framework au lieu des globales de module pour les données de requête.

Obtenir la sécurité gratuitement

Sécurisez votre code, votre cloud et votre environnement d'exécution dans un système central.
Trouvez et corrigez rapidement et automatiquement les vulnérabilités.

Aucune carte de crédit n'est requise | Scanner les résultats en 32sec.