Aikido

Pourquoi les variables globales provoquent 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 non 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 provenant d'autres utilisateurs. Cela crée des vulnérabilités de sécurité où les données de session, les jetons d'authentification ou les informations personnelles de l'utilisateur A peuvent fuir vers l'utilisateur B.

Pourquoi c'est important

Implications 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 inter-requêtes. L'état d'authentification, les données de session ou les informations personnelles d'un utilisateur deviennent visibles pour d'autres utilisateurs, violant ainsi les limites de confidentialité et de sécurité.

Conditions de concurrence : Lorsque plusieurs requêtes concurrentes modifient la même variable globale, il y a de fortes chances de comportement imprévisible. Les données de l'utilisateur A peuvent être écrasées par la requête de l'utilisateur B en cours de traitement, entraînant des calculs incorrects, un état corrompu ou la visualisation des données d'autres utilisateurs.

Complexité du débogage : Les problèmes causés par la mise en cache de variables globales sont notoirement difficiles à reproduire car ils dépendent du timing des requêtes et de la concurrence. Les bugs apparaissent par intermittence en production sous charge mais se manifestent rarement lors des tests de développement en mode mono-thread.

Fuites de mémoire : Les variables globales qui accumulent des données sans nettoyage augmentent de manière illimitée au fil du temps. Chaque requête ajoute davantage de données aux caches ou tableaux globaux, épuisant finalement la mémoire du serveur et nécessitant des redémarrages de 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 c'est incorrect : Les variables globales currentUser et requestData persistent entre les requêtes. Lorsque plusieurs requêtes s'exécutent simultanément, la requête de l'utilisateur B peut écraser currentUser pendant que buildUserProfile() de l'utilisateur A est toujours en cours d'exécution, ce qui fait que l'utilisateur A voit 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 c'est important : Toutes les données spécifiques à la requête sont stockées dans des variables locales dont la portée est limitée au gestionnaire de requêtes. Chaque requête possède un état isolé qui ne peut pas fuir vers d'autres requêtes concurrentes. Les fonctions reçoivent les données via des paramètres plutôt que d'accéder à l'état global, éliminant ainsi les conditions de concurrence.

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 connexions ou les caches en lecture seule. Lorsque l'état global est nécessaire, utilisez des mécanismes de contrôle de concurrence appropriés et assurez-vous que les données ne sont jamais spécifiques à un utilisateur.

FAQ

Des questions ?

Quand est-il sûr d'utiliser des variables globales dans Node.js ?

Les variables globales sont sûres pour les données en lecture seule qui s'appliquent à toutes les requêtes : configuration d'application, pools de connexions de 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 de manière globale. Si vous devez mettre en cache des données globalement, assurez-vous qu'elles sont correctement indexées et que l'accès est thread-safe, ou utilisez des solutions de mise en cache appropriées comme 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 à l'échelle du fichier) se comportent exactement comme des variables globales dans Node.js. Elles persistent à travers toutes les requêtes et sont partagées par tous les gestionnaires de requêtes concurrents. Les mêmes risques de fuite de données et de conditions de concurrence s'appliquent. Traitez les variables au niveau du module avec la même prudence que les variables globales explicites.

Comment partager des données entre les middlewares et les gestionnaires de routes ?

Utilisez les propriétés d'objet de requête fournies par votre framework. Express fournit `req.locals` ou des propriétés personnalisées sur `req`. Fastify dispose de `request.decorateRequest()`. Ces objets sont liés à la requête (request-scoped) et sont automatiquement nettoyés une fois la requête terminée, évitant ainsi les fuites entre les requêtes.

Qu'en est-il des patterns singleton et des instances de classe ?

Les instances singleton au niveau du module sont un état global. Si elles contiennent des données spécifiques à la requête, les mêmes problèmes s'appliquent. Concevez les singletons pour qu'ils soient sans état (stateless) ou qu'ils ne contiennent que la configuration. Pour les opérations avec état (stateful), créez de nouvelles instances par requête ou utilisez des modèles de fabrique qui garantissent l'isolation.

Comment détecter ces problèmes pendant le développement ?

Exécutez des tests de charge avec des requêtes concurrentes en utilisant différents contextes utilisateur. Les conditions de concurrence (race conditions) et les fuites de données n'apparaissent souvent pas avec les tests séquentiels. Utilisez des outils comme Apache Bench ou autocannon pour générer une charge concurrente. Ajoutez une journalisation incluant les IDs de requête pour suivre l'apparition de données d'une requête dans une autre.

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

Partiellement. Chaque invocation Lambda obtient un environnement d'exécution frais, 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 vous fiez pas à la réinitialisation des variables globales. Suivez les mêmes pratiques : conservez les données de 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 multi-thread ou asynchrone, donc 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 à portée de requête. Django dispose d'objets de requête. Utilisez les mécanismes fournis par le framework au lieu des variables globales de module pour les données de requête.

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.