Course contentsShow
← Node.js
Lesson 1813 of 1,98016. Security Hardening and Threat MitigationFree lesson

Protecting Shared Memory and Global State

Implement locking mechanisms and scope isolation to prevent cross-request state contamination.

In this lesson, you will learn how to isolate state and implement synchronization patterns to prevent sensitive data from leaking across concurrent user requests.

The Dangers of Shared Mutable State

In Node.js, the event loop processes requests asynchronously, but your server instance remains a single process. If you store request-specific data in a global variable or a module-level object, that data persists between requests. If Request A modifies a global object and then initiates an asynchronous operation (like a database query), Request B might modify that same object before Request A finishes. This leads to state contamination, where user data is inadvertently leaked or corrupted.

Implementing Scope Isolation

The first line of defense is lexical scoping. Avoid declaring objects at the module scope if they hold request-specific information. Instead, pass state through function arguments or use AsyncLocalStorage from the node:async_hooks module. AsyncLocalStorage allows you to store data that is contextually tied to a specific asynchronous execution chain, ensuring it remains isolated from concurrent requests.

Locking Mechanisms

When you must perform non-atomic operations on shared resources (like a shared configuration cache or an in-memory buffer), you need to prevent race conditions. Since Node.js is single-threaded, you don't face traditional multi-threaded CPU race conditions, but you do face "logical" race conditions during await points. Use a simple promise-based mutex to ensure that only one operation can mutate the shared state at a time:

let isLocked = false;
async function updateSharedState(data) {
  while (isLocked) await new Promise(resolve => setTimeout(resolve, 10));
  isLocked = true;
  try {
    // Perform sensitive mutation
    sharedState.value = data;
  } finally {
    isLocked = false;
  }
}

By forcing tasks to queue up, you ensure that the integrity of your state remains intact despite the non-blocking nature of your I/O operations. Always prefer stateless design patterns; if the state doesn't need to be global, keep it localized to the request handler scope to eliminate the risk entirely.

Key Takeaway: Prevent cross-request contamination by favoring request-scoped variables via AsyncLocalStorage and implementing mutex-like synchronization for any necessary shared mutable state.