Shared Mutable State

ID

javascript.shared_mutable_state

Severity

low

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Synchronization

Language

JavaScript

Tags

ASVS50:v15.4.1, CWE:362, CWE:567, NIST.SP.800-53, PCI-DSS:6.5.6

Description

Detects shared mutable state in Node.js server code: module-level bindings and collections, class statics, the fields of an instance a module builds when it loads, and the fields of a NestJS provider or controller (Nest builds one per application unless its scope is REQUEST or TRANSIENT). A module is loaded once per process, so this state is shared by every request the server handles.

Node runs one callback at a time, so a plain write cannot race. Requests interleave at an await, and the rule reports:

  • a read-modify-write across an await: the state is read, an await yields to other requests, and the write computed from the stale read (or decided by it) loses their updates;

  • a caller’s value stashed across an await: a request’s data is written to the state and read back after an await, when another request may have replaced it with its own;

  • per-caller data kept at module level: a module-level collection gains an entry keyed by, or holding, a value that comes from a request, and nothing ever removes one. The request may reach it through the functions it calls, in any module; an entry holding the server’s own data, such as a record it loaded, is not reported;

  • a write to a SharedArrayBuffer view that is not an Atomics call: worker threads run in parallel.

When the value written comes from a request, or from a function parameter, one caller’s data can reach another, and that finding is raised to high. One issue is reported per piece of state, at its worst write.

Rationale

A read-modify-write across an await:

let balance = 100;

router.post('/withdraw', async (req, res) => {
  const current = balance;
  await audit(req.body.amount);            // another withdrawal runs here
  balance = current - req.body.amount;     // and is overwritten
  res.json({ balance });
});

The caller stashed in a module binding:

let currentUser = null;

router.get('/me', async (req, res) => {
  currentUser = req.user;
  await audit(req.user.id);
  res.json({ user: currentUser });         // may be another request's user
});

A singleton provider’s field:

@Injectable()
export class InventoryService {
  private stock = 0;

  async take(sku: string, count: number) {
    const left = this.stock;
    await reserve(sku);
    this.stock = left - count;
  }
}

Remediation

  1. Keep request data in the request: pass it through arguments and return values, or use AsyncLocalStorage for per-request context.

  2. Do the read and the write in one turn: compute the new value after the await from the current state (balance -= amount after the awaited call), or move shared counters to a store with atomic operations (a database, Redis INCR).

  3. Serialize the critical section with an async lock:

    const { Mutex } = require('async-mutex');
    const balanceMutex = new Mutex();
    
    router.post('/withdraw', async (req, res) => {
      await balanceMutex.runExclusive(async () => {
        const current = balance;
        await audit(req.body.amount);
        balance = current - req.body.amount;
      });
      res.json({ balance });
    });
  4. Bound per-user collections: remove entries when they end, or keep them in a session store or a cache with eviction.

Not Reported

  • A write with no await between it and the read it depends on, and a check-then-set done in one turn before the await (if (!busy) { busy = true; await work(); busy = false; }).

  • A write inside mutex.runExclusive(…) or after await mutex.acquire() on a lock.

  • A lazy initialization whose value is not a parameter: if (!client) client = await connect() on state declared empty, or after if (cached) return cached.

  • A load-once flag, only ever set to true, and the plain writes under if (!loaded).

  • Writes while the module loads, in a constructor, a NestJS lifecycle hook (onModuleInit, onApplicationBootstrap), a bootstrap/main function, an immediately-invoked function, a listen callback, and in a helper only those call, directly or through other such helpers.

  • An instance a request builds for itself, and a request-scoped NestJS provider.

  • A collection keyed by a constant or by a value of a closed type (a union of literals such as 'disabled' | 'expired', an enum, or an element of a constant list of literals such as typeof KINDS[number] over ['draft', 'final'] as const), or from which entries are removed.

  • A collection whose entries hold only the server’s own data: no request reaches the key or the value, through any caller.

  • Test code, and browser code (a module importing React, Vue, Angular or another UI framework, other than for its types alone, reading window or document, or kept under a client directory).

© 2026 Xygeni. All rights reserved