Feb 13, 2026·6 min read·59 visits
If you use `next-mdx-remote` versions 4.3.0 through 5.0.0 to render user-provided content, your server is wide open. The library fails to sanitize JavaScript expressions inside MDX files. An attacker can submit a post containing `{process.exit()}` or worse, and your server will execute it during rendering. Upgrade to v6.0.0 immediately to disable inline JS execution.
A critical Remote Code Execution (RCE) vulnerability in `next-mdx-remote` allows attackers to execute arbitrary commands on the server by injecting malicious JavaScript expressions into MDX content. This flaw exploits the library's default behavior of evaluating code within Markdown during the serialization process, effectively turning a harmless blog post into a weaponized payload.
We all love Markdown. It's the lingua franca of the developer web—simple, clean, and safe. Or so we thought. Enter MDX, the cool cousin that lets you import React components directly into your Markdown files. It’s a brilliant idea for developer blogs and documentation sites. But there's a catch: to make that magic happen, the text has to be compiled into code. And whenever you turn user input into code, you are walking a razor's edge over a pit of vipers.
CVE-2026-0969 is what happens when that compilation process is too trusting. next-mdx-remote, a massively popular library for Next.js applications, facilitates loading MDX content from anywhere (database, CMS, local files). The problem? In versions prior to 6.0.0, it didn't just render the Markdown; it eagerly evaluated any JavaScript expression tucked inside a pair of curly braces {}.
Imagine a scenario where you have a comment section or a guest post feature that supports MDX. You think you're just rendering bold text and lists. The attacker, however, sees a direct line to your server's runtime. This isn't just Cross-Site Scripting (XSS)—this is full-blown Remote Code Execution (RCE) if the rendering happens on the server (SSR), which is exactly what Next.js is famous for.
The vulnerability lies in the serialize and compileMDX functions. These are the workhorses of the library, responsible for taking a raw string of MDX and transforming it into something React can render. In affected versions (4.3.0 to 5.0.0), the serialization process treats JavaScript expressions embedded in the Markdown as first-class citizens.
Technically, this is CWE-94: Improper Control of Generation of Code. The library assumes that the content passed to it is trusted. In the Jamstack era, this assumption often breaks down. Developers frequently fetch content from Headless CMS platforms where permissions might be lax, or worse, allow public submissions.
When next-mdx-remote parses the input, it uses a bundler (esbuild) internally to prepare the code. The flaw is that it didn't apply a default sandbox or strip out dangerous globals. It simply said, "Oh, you want to run Math.random()? Sure! Oh, you want to run process.kill()? Why not!" It failed to distinguish between benign logic (like formatting a date) and system-destroying logic.
Let's look at the "Smoking Gun". A typical implementation of next-mdx-remote looks innocent enough. You fetch a string from a database and pass it to serialize.
// Vulnerable Implementation
import { serialize } from 'next-mdx-remote/serialize';
export async function getStaticProps({ params }) {
// source could be from a file, a DB, or an API
const source = await getPostContent(params.slug);
// THE KILL ZONE:
// The compiler evaluates expressions inside 'source' here.
const mdxSource = await serialize(source);
return { props: { source: mdxSource } };
}The fix introduced in version 6.0.0 is a paradigm shift. Instead of allowing JS by default, they flipped the switch. Now, you have to explicitly opt-in to danger.
// Patched Implementation (v6.0.0+)
import { serialize } from 'next-mdx-remote/serialize';
const mdxSource = await serialize(source, {
parseFrontmatter: true,
mdxOptions: {
// New default behavior blocks JS execution
// If you manually set this to false, you are re-enabling the vulnerability
// unless you know exactly what you are doing.
blockJS: true
}
});The patch also introduces blockDangerousJS, a heuristic-based blocker that tries to intercept calls to eval, Function, and process. It's a safety net, but as any exploit dev knows, heuristics are meant to be bypassed. The real fix is disabling JS entirely.
Exploiting this is trivially easy if you control the input string. Because next-mdx-remote uses eval-like mechanisms to handle the compiled output, we can access the Node.js process global if the code runs on the server.
Here is what a weaponized payload looks like. It uses process.mainModule.require to bypass standard require restrictions if they exist, and then calls child_process to execute shell commands.
The Payload:
# My Innocent Blog Post
Here is a list of reasons why I love MDX:
1. It is flexible.
2. It allows components.
3. {process.mainModule.require('child_process').execSync('cat /etc/passwd').toString()}The Execution Flow:
getStaticProps (build time) or getServerSideProps (request time).serialize(). The bundler sees the code inside {} and executes it to resolve the value.cat /etc/passwd is embedded directly into the HTML of the page, or the command executes silently (e.g., a reverse shell) before the page even renders.> [!WARNING]
> If this vulnerability is triggered in getStaticProps, the RCE happens on the build server (often CI/CD pipelines). If triggered in getServerSideProps, it happens on the production web server.
The immediate fix is to upgrade to next-mdx-remote v6.0.0. This version changes the default behavior to blockJS: true. This effectively kills the attack vector by treating content inside curly braces as text rather than code, unless explicitly configured otherwise.
If you cannot upgrade, you must sanitize your inputs. However, sanitizing code is notoriously difficult. A simple regex looking for process or require is easily bypassed (e.g., global['pro' + 'cess']).
Defensive Strategy:
blockJS: false in your config.process.env, child_process, or execSync in the body, although this is a weak layer of defense against a determined attacker.CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
next-mdx-remote HashiCorp / Open Source | >= 4.3.0 < 6.0.0 | 6.0.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-94 |
| CVSS Score | 8.8 (High) |
| Attack Vector | Network |
| Exploit Maturity | Proof of Concept (High Probability) |
| Privileges Required | Low (User Input) |
| Confidentiality Impact | High |
| Integrity Impact | High |
| Availability Impact | High |
The software constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
A module allowlist bypass vulnerability (CVE-2026-92945 / GHSA-7q3f-wx44-378m) was identified in the vm2 sandboxing library prior to version 3.11.7. This flaw permits unauthenticated or untrusted code running within the sandbox environment to bypass explicit module restrictions. When the transitive resolution option is disabled, the system fails to validate file system path boundaries, allowing prefix-sharing sibling directories to be resolved and loaded, thereby escaping intended sandbox restrictions.
CVE-2026-92941 is a critical sandbox-escape and trust-manipulation vulnerability in the vm2 library (versions 3.11.3 to 3.11.6). This security flaw allows untrusted code executing within a NodeVM sandbox environment to compromise the global TLS trust store of the host Node.js process. By leveraging a design flaw where the host's native 'tls.setDefaultCACertificates' can be executed via a proxy wrapper, combined with a bridge unwrapping bypass in the 'url' module, an attacker can modify the process-wide default root Certificate Authorities. Consequently, all subsequent outbound TLS/HTTPS clients running on the host thread are forced to trust attacker-signed certificates, facilitating transparent Man-in-the-Middle (MitM) attacks. The vulnerability was resolved in version 3.11.7 of vm2 by introducing built-in member-level sanitization before applying read-only proxy wrappers.
A critical engine-level reachability failure in Node.js 26 running V8 14.6 allows attackers to escape the vm2 sandbox environment. When consecutive prototype properties are modified using sequential assignments, a V8 optimization bug fails to invalidate the PromiseThenLookupChain protector. By calling Promise.prototype.finally, the attacker bypasses the vm2 wrappers, hijacks the promise reaction using a custom constructor, triggers a calibrated stack overflow to capture a host-realm RangeError, and executes arbitrary shell commands on the host.
A critical sandbox escape vulnerability in the vm2 library allows sandboxed JavaScript code to bypass containment and execute arbitrary native code on the host process. This occurs when the host's builtin crypto module is exposed to the NodeVM environment. Although vm2 implements a read-only proxy layer to restrict direct modifications to host properties, it does not prevent invocation of host-level functions. By calling the crypto.setEngine API with a path to a malicious native library on disk, an attacker can trigger OpenSSL's dynamic module loader. The host's operating system loader immediately runs the dynamic library's initializers/constructors before verifying engine compatibility, leading to remote code execution in the context of the host process.
CVE-2026-92938 is a critical sandbox escape vulnerability in the vm2 library (versions 3.11.3 through 3.11.6) that allows arbitrary native code execution on the host when the node:sqlite built-in module is loaded inside a sandboxed NodeVM environment.
CVE-2026-92937 is a critical sandbox escape vulnerability in the `vm2` Node.js library. Due to a logical failure in checking direct invocation targets inside the Proxy bridge, an attacker can register Promise callbacks using `Function.prototype.call` or `Function.prototype.apply` indirection. This bypasses the error sanitization wrappers, delivering raw host error objects directly to sandboxed callbacks and allowing the attacker to escape the sandbox and execute arbitrary shell commands on the host.