Aikido

Why global variables cause data leaks in Node.js servers

Security

Rule
Avoid unintended global variable caching.In Node.js
and Python servers, global variables persist across
requests, causing data leaks and race conditions.

Supported languages: JavaScript, TypeScript, Python

Introduction

Global variables in Node.js servers persist for the lifetime of the process, not just a single request. When request handlers store user data in global variables, that data remains accessible to subsequent requests from different users. This creates security vulnerabilities where user A's session data, authentication tokens, or personal information leak to user B.

Why it matters

Security implications (data leaks): Global variables that cache user-specific data create cross-request data leaks. One user's authentication state, session data, or personal information becomes visible to other users, violating privacy and security boundaries.

Race conditions: When multiple concurrent requests modify the same global variable, there's a high chance of unpredictable behavior. User A's data can get overwritten by user B's request mid-processing, leading to incorrect calculations, corrupted state, or users seeing each other's data.

Debugging complexity: Issues caused by global variable caching are notoriously difficult to reproduce because they depend on request timing and concurrency. Bugs appear intermittently in production under load but rarely manifest in single-threaded development testing.

Memory leaks: Global variables that accumulate data without cleanup grow unbounded over time. Each request adds more data to global caches or arrays, eventually exhausting server memory and requiring process restarts.

Code examples

❌ Non-compliant:

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
    };
}

Why it's wrong: The global variables currentUser and requestData persist across requests. When multiple requests execute concurrently, user B's request can overwrite currentUser while user A's buildUserProfile() is still executing, causing user A to see user B's data.

✅ Compliant:

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
    };
}

Why this matters: All request-specific data is stored in local variables scoped to the request handler. Each request has isolated state that can't leak to other concurrent requests. Functions receive data through parameters rather than accessing global state, eliminating race conditions.

Conclusion

Keep all request-specific data in local variables or request objects provided by your framework. Use global variables only for truly shared state like configuration, connection pools, or read-only caches. When global state is necessary, use proper concurrency controls and ensure data is never user-specific.

FAQs

Got Questions?

When is it safe to use global variables in Node.js?

Global variables are safe for read-only data that applies to all requests: application configuration, database connection pools, compiled templates, or shared utilities. Never store request-specific or user-specific data globally. If you need to cache data globally, ensure it's keyed properly and access is thread-safe, or use proper caching solutions like Redis.

What about module-level variables that aren't explicitly global?

Module-level variables (const, let, var at file scope) behave exactly like globals in Node.js. They persist across all requests and are shared by all concurrent request handlers. The same data leak and race condition risks apply. Treat module-level variables with the same caution as explicit globals.

How do I share data between middleware and route handlers?

Use request object properties provided by your framework. Express provides req.locals or custom properties on req. Fastify has request.decorateRequest(). These objects are request-scoped and automatically cleaned up after the request completes, preventing leaks between requests.

What about singleton patterns and class instances?

Singleton instances at module level are global state. If they hold request-specific data, the same problems apply. Design singletons to be stateless or hold only configuration. For stateful operations, create new instances per request or use factory patterns that ensure isolation.

How do I detect these issues during development?

Run load tests with concurrent requests using different user contexts. Race conditions and data leaks often don't appear with sequential testing. Use tools like Apache Bench or autocannon to generate concurrent load. Add logging that includes request IDs to track when data from one request appears in another.

Does this apply to serverless functions like AWS Lambda?

Partially. Each Lambda invocation gets a fresh execution environment, but the container can be reused across invocations. Global variables persist between invocations that reuse the same container. Don't rely on globals being reset. Follow the same practices: keep request data in local scope.

What about Python WSGI/ASGI applications?

Same principles apply. Python web servers run multi-threaded or async, so module-level variables are shared across requests. Flask's g object and FastAPI's dependency injection provide request-scoped storage. Django has request objects. Use framework-provided mechanisms instead of module globals for request data.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.