Node.js added worker_threads as a stable module in Node 12 (2019) to let JavaScript run true parallel code inside one process, each worker executing in its own V8 isolate with its own event loop. That isolation is the entire selling point — and it is also where the security assumptions start to leak. Two mechanisms move data across the boundary: postMessage, which serializes values with the HTML structured clone algorithm and deep-copies them, and SharedArrayBuffer paired with Atomics, which hands multiple threads a single block of memory with no copying and no isolation at all. On January 21, 2025, Node.js shipped a security release patching CVE-2025-23083, a CVSS 7.7 flaw in which the diagnostics_channel module let code subscribed to worker-creation events capture references to Node's own internal worker instances — objects that were never meant to be reachable — and reuse their constructor to spawn new workers that bypassed the experimental --permission model entirely, affecting Node 20, 22, and 23. That single CVE is a concrete lesson: thread isolation in Node is a design intent, not a hard security boundary, and the APIs built to enforce it have already had bypasses. This post covers what actually moves between worker threads, where that creates real risk, and how to defend against it.
How does data actually move between worker threads?
Data moves between worker threads through two fundamentally different paths, and conflating them is the root of most worker_threads security mistakes. parentPort.postMessage() and worker.postMessage() use the HTML structured clone algorithm to serialize the value, producing a deep copy on the receiving side — the sender and receiver never touch the same memory, which is why passing a plain object or array between threads is safe by default. The same API can optionally transfer ownership of specific objects — ArrayBuffer, MessagePort, and (as of newer Node versions) FileHandle and KeyObject — meaning the sending thread loses access once the transfer completes. SharedArrayBuffer is the outlier: when passed to postMessage, it is not cloned or transferred, it is shared. Both threads read and write the same backing memory simultaneously, and Node does nothing to serialize or fence that access — that job belongs entirely to Atomics.wait(), Atomics.notify(), and related primitives that the developer must use correctly.
What goes wrong when threads share memory via SharedArrayBuffer?
Unsynchronized access to a SharedArrayBuffer opens the door to time-of-check-to-time-of-use (TOCTOU) race conditions in ordinary application logic, not just the browser-side Spectre side-channel concerns that SharedArrayBuffer is most often associated with. A concrete pattern: one thread validates a value stored in shared memory — say, checking that a balance or a permission flag is within an allowed range — and then acts on it a few instructions later, while a second thread with a reference to the same buffer overwrites that value in between the check and the use. Because there is no implicit locking, the "validated" read and the "used" read can observe different data, and the bug reproduces intermittently depending on scheduling, which makes it notoriously hard to catch in testing. The fix is architectural, not incidental: any shared, mutable region accessed from more than one thread needs an explicit Atomics-based lock or compare-and-swap around every read-then-act sequence, and code review should treat a SharedArrayBuffer reference passed into a worker as a synchronization obligation, not a convenience.
Why should postMessage payloads be treated as untrusted input?
postMessage payloads deserve the same scrutiny as any other untrusted input whenever a worker's job is to process attacker-influenced data — parsing an uploaded file, decoding a user-submitted document, or transforming request-derived content off the main thread. The structured clone algorithm itself is memory-safe and does not execute arbitrary code the way an insecure deserializer or eval would, but that safety only covers the transport. Once the cloned object lands inside the worker, it is handed to application code that may parse it, index into it, or pass it to a downstream library — and if that library has its own deserialization or parsing bugs, the worker boundary provides no protection. Treat the worker as a separate trust zone: validate and sanitize payloads on the way in exactly as you would at an HTTP handler, and never assume that "it came over postMessage from our own main thread" makes a value safe if that main thread's data originated from a user.
How did CVE-2025-23083 break Node's own isolation guarantees?
CVE-2025-23083 broke isolation by exploiting an API that was never designed with worker security in mind: diagnostics_channel, a general-purpose instrumentation module for tracing internal Node.js events. Code subscribed to diagnostics_channel's worker-creation notifications received references to workers Node spins up internally for its own runtime purposes — not just user-created ones — and those internal worker objects carried a constructor that, when extracted and reinvoked, could instantiate new Worker instances outside the scope the --permission flag was supposed to enforce. Node.js fixed this in the January 21, 2025 security release by scoping the events diagnostics_channel exposes so internal workers are no longer indistinguishable from user-created ones. The lesson generalizes: a permission or sandboxing model is only as strong as every API adjacent to it, and an observability hook is a plausible place for a boundary to leak from — a pattern worth checking for in any custom instrumentation layered on top of worker pools.
What practical steps reduce worker_threads risk?
Several concrete controls cut real risk without requiring you to avoid worker_threads altogether. Never pass secrets — API keys, session tokens, decryption keys — into a worker that also parses untrusted input; a single processing bug in that worker now has both the secret and an attacker-controlled payload in scope. Bound every worker's resource consumption with the resourceLimits option (maxOldGenerationSizeMb, maxYoungGenerationSizeMb, stackSizeMb) so a runaway or hostile task cannot exhaust process memory. On Node 20 and later, the experimental --permission model plus APIs like markAsUncloneable() and markAsUntransferable() let you explicitly block specific objects from ever crossing a postMessage boundary, which is useful defense-in-depth even though CVE-2025-23083 shows the model itself needs to stay patched. And keep the Node.js runtime current — permission-model bypasses are exactly the kind of finding that only shows up in a scanner watching the runtime version, not in application source review.
How does Safeguard help?
Safeguard doesn't ship a worker_threads-specific detector, but two existing engines cover the parts of this risk that are mechanically checkable. Safeguard's SCA continuously matches your resolved Node.js runtime and package versions against CVE and GHSA advisories enriched with EPSS and CISA KEV data, which is exactly the mechanism that would flag a CVE-2025-23083-class Node permission-model bypass in your fleet the moment it's disclosed — before it becomes a targeted attack path. Safeguard's SAST traces untrusted-input dataflow from source to sink across JavaScript and TypeScript, which is directly relevant to catching unvalidated request- or upload-derived data that reaches a postMessage call or a worker entry point, even though it doesn't yet reason about SharedArrayBuffer race conditions specifically. Together, they close the gap between "we know worker isolation has assumptions" and "we know when our own code or runtime violates them."