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
awaityields 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
SharedArrayBufferview that is not anAtomicscall: 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
-
Keep request data in the request: pass it through arguments and return values, or use
AsyncLocalStoragefor per-request context. -
Do the read and the write in one turn: compute the new value after the
awaitfrom the current state (balance -= amountafter the awaited call), or move shared counters to a store with atomic operations (a database, RedisINCR). -
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 }); }); -
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
awaitbetween it and the read it depends on, and a check-then-set done in one turn before theawait(if (!busy) { busy = true; await work(); busy = false; }). -
A write inside
mutex.runExclusive(…)or afterawait mutex.acquire()on a lock. -
A lazy initialization whose value is not a parameter:
if (!client) client = await connect()on state declared empty, or afterif (cached) return cached. -
A load-once flag, only ever set to
true, and the plain writes underif (!loaded). -
Writes while the module loads, in a constructor, a NestJS lifecycle hook (
onModuleInit,onApplicationBootstrap), abootstrap/mainfunction, an immediately-invoked function, alistencallback, 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 astypeof 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
windowordocument, or kept under a client directory).