Feb 13, 2026·6 min read·16 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.