CVE-2026-0969
Description
The serialize function used to compile MDX in next-mdx-remote is vulnerable to arbitrary code execution due to insufficient sanitization of MDX content. This vulnerability, CVE-2026-0969, is fixed in next-mdx-remote 6.0.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
The serialize function in next-mdx-remote is vulnerable to arbitrary code execution due to insufficient sanitization of MDX content, fixed in version 6.0.0.
Vulnerability
Overview
The serialize function in the next-mdx-remote library, used to compile MDX content, is vulnerable to arbitrary code execution. The root cause is insufficient sanitization of MDX input, allowing an attacker to inject malicious JavaScript that gets evaluated during the serialization process [1][2]. This vulnerability is assigned CVE-2026-0969 and has a CVSS v3 score of 8.8 (High).
Exploitation
Details
An attacker can exploit this vulnerability by providing crafted MDX content containing JavaScript expressions or dangerous code. The serialize function processes this content server-side, typically within Next.js getStaticProps or getServerSideProps in Next.js applications. No authentication is required if the attacker can control the MDX source (e.g., from user input, database, or external file). The attack surface is any application using `next-mdx-remote to compile untrusted MDX content [1][2].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript code on the server where serialize is called. This can lead to full server compromise, data exfiltration, or further lateral movement within the infrastructure. The impact is severe as it bypasses typical sandboxing of MDX content [2].
Mitigation
The vulnerability is fixed in next-mdx-remote version 6.0.0. The fix introduces two new options: blockJS (defaults to true) to block JavaScript expressions in MDX, and blockDangerousJS (defaults to true) to prevent access to dangerous globals like eval, Function, process, and require [3][4]. Users should upgrade to version 6.0.0 immediately. Note that the project is archived and no longer supported [1].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
next-mdx-remotenpm | >= 4.3.0, < 6.0.0 | 6.0.0 |
Affected products
1- Range: <6.0.0
Patches
14d527fdcaed9Merge pull request #498 from hashicorp/version-6.0.0
13 files changed · +487 −175
package.json+3 −2 modified@@ -1,7 +1,7 @@ { "name": "next-mdx-remote", "description": "utilities for loading mdx from any remote source as data, rather than as a local import", - "version": "5.0.0", + "version": "6.0.0", "author": "Jeff Escalante", "bugs": { "url": "https://github.com/hashicorp/next-mdx-remote/issues" @@ -16,7 +16,8 @@ "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", - "unist-util-remove": "^3.1.0", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" },
package-lock.json+152 −166 modified@@ -1,18 +1,19 @@ { "name": "next-mdx-remote", - "version": "4.4.1", + "version": "6.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "next-mdx-remote", - "version": "4.4.1", + "version": "6.0.0", "license": "MPL-2.0", "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", - "unist-util-remove": "^3.1.0", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, @@ -49,8 +50,7 @@ "npm": ">=7" }, "peerDependencies": { - "react": ">=16.x <=18.x", - "react-dom": ">=16.x <=18.x" + "react": ">=16" } }, "node_modules/@ampproject/remapping": { @@ -1036,6 +1036,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@hashicorp/remark-plugins/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@hashicorp/remark-plugins/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/@hashicorp/remark-plugins/node_modules/vfile": { "version": "4.2.1", "dev": true, @@ -1249,10 +1280,6 @@ "version": "1.0.5", "license": "MIT" }, - "node_modules/@mdx-js/mdx/node_modules/@types/unist": { - "version": "3.0.2", - "license": "MIT" - }, "node_modules/@mdx-js/mdx/node_modules/collapse-white-space": { "version": "2.1.0", "license": "MIT", @@ -1272,42 +1299,6 @@ "node": ">= 8" } }, - "node_modules/@mdx-js/mdx/node_modules/unist-util-is": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/mdx/node_modules/unist-util-visit": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/mdx/node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/@mdx-js/react": { "version": "3.0.1", "license": "MIT", @@ -5163,6 +5154,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-compact/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-compact/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.0", "license": "MIT", @@ -5391,42 +5413,6 @@ "version": "3.0.2", "license": "MIT" }, - "node_modules/mdast-util-to-hast/node_modules/unist-util-is": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast/node_modules/unist-util-visit": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast/node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdast-util-to-markdown": { "version": "2.1.0", "license": "MIT", @@ -5464,42 +5450,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-is": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdast-util-to-string": { "version": "4.0.0", "license": "MIT", @@ -7834,6 +7784,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark/node_modules/vfile": { "version": "4.2.1", "dev": true, @@ -9367,12 +9348,14 @@ "license": "MIT" }, "node_modules/unist-util-remove": { - "version": "3.1.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", @@ -9395,98 +9378,101 @@ "version": "3.0.2", "license": "MIT" }, - "node_modules/unist-util-remove-position/node_modules/unist-util-is": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/unist-util-remove/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" }, - "node_modules/unist-util-remove-position/node_modules/unist-util-visit": { - "version": "5.0.0", + "node_modules/unist-util-remove/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-remove-position/node_modules/unist-util-visit-parents": { - "version": "6.0.1", + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-remove/node_modules/unist-util-is": { - "version": "5.0.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/unist-util-stringify-position/node_modules/@types/unist": { + "version": "3.0.2", + "license": "MIT" }, - "node_modules/unist-util-remove/node_modules/unist-util-visit-parents": { + "node_modules/unist-util-visit": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { - "@types/unist": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-stringify-position/node_modules/@types/unist": { - "version": "3.0.2", + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, - "node_modules/unist-util-visit": { - "version": "2.0.3", - "dev": true, + "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit-parents": { - "version": "3.1.0", - "dev": true, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective",
.prettierrc+2 −2 modified@@ -1,4 +1,4 @@ { - "semi": false, - "singleQuote": true + "singleQuote": true, + "semi": false }
README.md+20 −1 modified@@ -292,7 +292,7 @@ export async function getStaticProps() { This library exposes a function and a component, `serialize` and `<MDXRemote />`. These two are purposefully isolated into their own files -- `serialize` is intended to be run **server-side**, so within `getStaticProps`, which runs on the server/at build time. `<MDXRemote />` on the other hand is intended to be run on the client side, in the browser. -- **`serialize(source: string, { mdxOptions?: object, scope?: object, parseFrontmatter?: boolean })`** +- **`serialize(source: string, { mdxOptions?: object, scope?: object, parseFrontmatter?: boolean, blockJS?: boolean, blockDangerousJS?: boolean })`** **`serialize`** consumes a string of MDX. It can also optionally be passed options which are [passed directly to MDX](https://mdxjs.com/docs/extending-mdx/), and a scope object that can be included in the MDX scope. The function returns an object that is intended to be passed into `<MDXRemote />` directly. @@ -313,6 +313,13 @@ This library exposes a function and a component, `serialize` and `<MDXRemote />` }, // Indicates whether or not to parse the frontmatter from the MDX source parseFrontmatter: false, + // Block JavaScript expressions in MDX (e.g., {variable}, {func()}) + // When true, these expressions are removed. Defaults to true. + blockJS: true, + // Provides a best effort option to block dangerous JavaScript when blockJS is false (JS is allowed). + // Prevents access to eval, Function, process, require, and other dangerous globals. + // Only applies when blockJS is false. Defaults to true for security. + blockDangerousJS: true, } ) ``` @@ -383,6 +390,18 @@ This library evaluates a string of JavaScript on the client side, which is how i If you have a CSP on your website that disallows code evaluation via `eval` or `new Function()`, you will need to loosen that restriction in order to utilize `next-mdx-remote`, which can be done using [`unsafe-eval`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#common_sources). +### JavaScript Expressions in MDX + +By default, JavaScript expressions (like `{variable}` or `{func()}`) are **disabled for security** for versions 6.0.0 and above. + +If you need to enable JavaScript expressions: + +1. **Trusted content with protection (recommended)**: Set `blockJS: false` in serialize options. By default, `blockDangerousJS: true` is enabled, which will provide a best effort option to blocks dangerous operations like `eval`, `Function`, `process`, `require`, and other globals that could lead to remote code execution (RCE). + +2. **Completely trusted content only**: Set both `blockJS: false` and `blockDangerousJS: false` to allow all JavaScript. + +**Warning:** Only set `blockDangerousJS: false` if you completely trust the MDX content source. This removes critical security protections and could allow RCE attacks. + ## TypeScript This project does include native types for TypeScript use. Both `serialize` and `<MDXRemote />` have types normally as you'd expect, and the library also exports a type which you can use to type the result of `getStaticProps`.
src/plugins/remove-dangerous-javascript-expressions.ts+215 −0 added@@ -0,0 +1,215 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import type { Plugin } from 'unified' +import type { Program } from 'estree-jsx' + +// Dangerous identifiers +const BLOCKED_GLOBALS = [ + 'eval', + 'Function', + 'AsyncFunction', + 'GeneratorFunction', + 'FunctionConstructor', + 'require', + 'process', + 'global', + 'globalThis', + 'module', + 'exports', + '__dirname', + '__filename', + 'child_process', + 'fs', + 'net', + 'http', + 'https', + 'vm', + 'worker_threads', + 'Reflect', +] + +// Built-in constructors that can be used to reach Function constructor +const BUILTIN_CONSTRUCTORS = [ + 'Object', + 'Array', + 'String', + 'Number', + 'Boolean', + 'Symbol', + 'Error', + 'Date', + 'RegExp', + 'Promise', + 'Proxy', + 'Reflect', + 'WeakMap', + 'WeakSet', + 'Map', + 'Set', +] + +const BLOCKED_PROPERTIES = [ + 'constructor', + 'prototype', + '__proto__', + 'eval', + 'Reflect', + 'Function', + 'AsyncFunction', + 'GeneratorFunction', + 'FunctionConstructor', + 'require', +] + +export const CreateRemoveDangerousCallsPlugin = ( + blocked_globals?: string[], + blocked_properties?: string[] +): Plugin<any[], Program> => { + return function () { + return function (tree: Program) { + walk( + tree, + blocked_globals ?? BLOCKED_GLOBALS, + blocked_properties ?? BLOCKED_PROPERTIES + ) + return tree // Must return the tree for RSC + } + } +} + +function walk( + node: any, + blocked_globals: string[], + blocked_properties: string[] +): void { + if (!node || typeof node !== 'object') return + + // Block dangerous identifiers + if (node.type === 'Identifier' && blocked_globals.includes(node.name)) { + // Only block if it's not a property name or parameter + const parent = node.parent + const isProperty = + parent?.type === 'MemberExpression' && + parent.property === node && + !parent.computed + const isParam = + parent?.type === 'FunctionDeclaration' || + parent?.type === 'FunctionExpression' + + if (!isProperty && !isParam) { + throw new Error(`Security: Access to '${node.name}' is not allowed`) + } + } + + // Block function calls to dangerous globals + if (node.type === 'CallExpression' && node.callee?.type === 'Identifier') { + if (blocked_globals.includes(node.callee.name)) { + throw new Error(`Security: ${node.callee.name}() calls are not allowed`) + } + } + + // Block function calls on computed properties of dangerous globals + // e.g., Object['constructor'](...), Object[expr](...) + if ( + node.type === 'CallExpression' && + node.callee?.type === 'MemberExpression' && + node.callee.computed && + node.callee.object?.type === 'Identifier' && + blocked_globals.includes(node.callee.object.name) + ) { + throw new Error( + `Security: Function calls on computed properties of '${node.callee.object.name}' are not allowed` + ) + } + + // Block function calls on computed properties of built-in constructors + // This prevents: Object[['constructor'].join('')](...), Array[expr](...) + // But allows: Object[expr] for reading (e.g., Object[key]) + if ( + node.type === 'CallExpression' && + node.callee?.type === 'MemberExpression' && + node.callee.computed && + node.callee.object?.type === 'Identifier' && + BUILTIN_CONSTRUCTORS.includes(node.callee.object.name) + ) { + throw new Error( + `Security: Function calls on computed properties of '${node.callee.object.name}' are not allowed` + ) + } + + // Block member expressions to dangerous properties + if (node.type === 'MemberExpression') { + const prop = node.property + + // obj.constructor, obj.prototype, etc. + if ( + prop?.type === 'Identifier' && + !node.computed && + blocked_properties.includes(prop.name) + ) { + throw new Error(`Security: .${prop.name} access is not allowed`) + } + + // obj["constructor"], obj['prototype'], etc. + if ( + prop?.type === 'Literal' && + blocked_properties.includes(String(prop.value)) + ) { + throw new Error(`Security: ["${prop.value}"] access is not allowed`) + } + + // Block computed property access on dangerous globals only + // Built-in constructors (Object, Array, etc.) are NOT blocked here for reading + // They are only blocked when CALLING the result (see CallExpression check above) + if ( + node.computed && + node.object?.type === 'Identifier' && + blocked_globals.includes(node.object.name) + ) { + throw new Error( + `Security: Computed property access on '${node.object.name}' is not allowed` + ) + } + + // Block access to blocked global objects + if ( + node.object?.type === 'Identifier' && + blocked_globals.includes(node.object.name) + ) { + throw new Error( + `Security: Access to '${node.object.name}' properties is not allowed` + ) + } + } + + // Block dynamic imports + // if (node.type === "ImportExpression") { + // throw new Error("Security: Dynamic import() is not allowed"); + // } + + // Block new Function() + if (node.type === 'NewExpression' && node.callee?.type === 'Identifier') { + if (blocked_globals.includes(node.callee.name)) { + throw new Error(`Security: new ${node.callee.name}() is not allowed`) + } + } + + // Recurse through all child nodes + for (const key in node) { + if (key === 'parent' || key === 'position') continue + + const value = node[key] + if (Array.isArray(value)) { + value.forEach((child) => { + if (child && typeof child === 'object') { + walk(child, blocked_globals, blocked_properties) + } + }) + } else if (value && typeof value === 'object') { + walk(value, blocked_globals, blocked_properties) + } + } +}
src/plugins/remove-javascript-expressions.ts+60 −0 added@@ -0,0 +1,60 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { visit, SKIP } from 'unist-util-visit' +import type { Plugin } from 'unified' + +/** + * Remark plugin that removes JavaScript expressions from MDX. + * This blocks patterns like {variable} or {func()} that enable code execution. + * + * Safe patterns (preserved): + * - JSX: <Component /> + * - Markdown: # Heading, **bold**, etc. + * + * Blocked patterns: + * - JS expressions: {variable}, {func()}, {obj.prop} + * - JSX attribute expressions: <Component prop={value} /> + */ +export const removeJavaScriptExpressions: Plugin = () => { + return (tree: any) => { + visit(tree, (node: any, index: number | undefined, parent: any) => { + // Remove mdxFlowExpression and mdxTextExpression nodes (JS expressions in MDX) + if ( + node.type === 'mdxFlowExpression' || + node.type === 'mdxTextExpression' + ) { + // Remove this node from parent + if (parent && typeof index === 'number') { + parent.children.splice(index, 1) + return [SKIP, index] + } + } + + // Remove JavaScript expressions from JSX attribute values + if ( + node.type === 'mdxJsxFlowElement' || + node.type === 'mdxJsxTextElement' + ) { + if (node.attributes) { + node.attributes = node.attributes.filter((attr: any) => { + // Keep literal values, remove expression values + if (attr.type === 'mdxJsxAttribute') { + // If the value is null (boolean attribute) or a literal string, keep it + return ( + attr.value === null || + typeof attr.value === 'string' || + (attr.value && + attr.value.type !== 'mdxJsxAttributeValueExpression') + ) + } + // Remove spread attributes entirely as they're JS expressions + return attr.type !== 'mdxJsxExpressionAttribute' + }) + } + } + }) + } +}
src/serialize.ts+15 −2 modified@@ -9,20 +9,28 @@ import { matter } from 'vfile-matter' import { createFormattedMDXError } from './format-mdx-error.js' import { removeImportsExportsPlugin } from './plugins/remove-imports-exports.js' +import { removeJavaScriptExpressions } from './plugins/remove-javascript-expressions.js' +import { CreateRemoveDangerousCallsPlugin } from './plugins/remove-dangerous-javascript-expressions.js' // types import type { MDXRemoteSerializeResult, SerializeOptions } from './types.js' function getCompileOptions( mdxOptions: SerializeOptions['mdxOptions'] = {}, - rsc: boolean = false + rsc: boolean = false, + blockJS: boolean = true, + blockDangerousJS: boolean = true ): CompileOptions { const areImportsEnabled = mdxOptions.useDynamicImport ?? false // don't modify the original object when adding our own plugin // this allows code to reuse the same options object const remarkPlugins = [ ...(mdxOptions.remarkPlugins || []), ...(areImportsEnabled ? [] : [removeImportsExportsPlugin]), + ...(blockJS ? [removeJavaScriptExpressions] : []), + ...(!blockJS && blockDangerousJS + ? [CreateRemoveDangerousCallsPlugin()] + : []), ] return { @@ -47,6 +55,8 @@ export async function serialize< scope = {}, mdxOptions = {}, parseFrontmatter = false, + blockJS = true, + blockDangerousJS = true, }: SerializeOptions = {}, rsc: boolean = false ): Promise<MDXRemoteSerializeResult<TScope, TFrontmatter>> { @@ -60,7 +70,10 @@ export async function serialize< let compiledMdx: VFile try { - compiledMdx = await compile(vfile, getCompileOptions(mdxOptions, rsc)) + compiledMdx = await compile( + vfile, + getCompileOptions(mdxOptions, rsc, blockJS, blockDangerousJS) + ) } catch (error: any) { throw createFormattedMDXError(error, String(vfile)) }
src/types.ts+10 −0 modified@@ -21,6 +21,16 @@ export interface SerializeOptions { * Indicate whether or not frontmatter should be parsed out of the MDX. Defaults to false */ parseFrontmatter?: boolean + /** + * Block JavaScript expressions in MDX. When true, patterns like {variable} or {func()} + * will be removed while preserving JSX components and standard Markdown. Defaults to true + */ + blockJS?: boolean + /** + * Provides a best effort option to block dangerous JavaScript expressions when JS is enabled. Prevents access to eval, Function, + * process, and other dangerous globals. Defaults to true for security. + */ + blockDangerousJS?: boolean } /**
__tests__/fixtures/basic/pages/index.jsx+1 −0 modified@@ -60,6 +60,7 @@ export async function getStaticProps() { const mdxSource = await serialize(source, { mdxOptions: { remarkPlugins: [paragraphCustomAlerts] }, parseFrontmatter: true, + blockJS: false, }) return { props: { mdxSource } }
__tests__/fixtures/rsc/app/app-dir-mdx/compile-mdx/page.js+1 −0 modified@@ -25,6 +25,7 @@ export default async function Page() { options: { mdxOptions: { remarkPlugins: [] }, parseFrontmatter: true, + blockJS: false, }, })
__tests__/fixtures/rsc/app/app-dir-mdx/mdxremote/page.js+1 −0 modified@@ -27,6 +27,7 @@ export default async function Page() { options={{ mdxOptions: { remarkPlugins: [] }, parseFrontmatter: true, + blockJS: false, }} /> </>
__tests__/serialize.test.tsx+3 −2 modified@@ -55,6 +55,7 @@ describe('serialize', () => { scope: { bar: 'test', }, + blockJS: false, }) expect(result).toMatchInlineSnapshot(`"<p>test</p>"`) }) @@ -151,7 +152,7 @@ export const bar = 'bar'` const result = await renderStatic( `<Test content={<>Rendering a fragment</>} />`, - { components } + { components, blockJS: false } ) expect(result).toMatchInlineSnapshot(`"Rendering a fragment"`) }) @@ -195,7 +196,7 @@ hello: world --- # Hello {frontmatter.hello}`, - { parseFrontmatter: true } + { parseFrontmatter: true, blockJS: false } ) expect(result).toMatchInlineSnapshot(`"<h1>Hello world</h1>"`)
__tests__/utils.tsx+4 −0 modified@@ -30,11 +30,15 @@ export async function renderStatic( scope = {}, mdxOptions, parseFrontmatter, + blockJS, + blockDangerousJS, }: Partial<SerializeOptions & Pick<MDXRemoteProps, 'components'>> = {} ): Promise<string> { const mdxSource = await serialize(mdx, { mdxOptions, parseFrontmatter, + blockJS, + blockDangerousJS, }) return ReactDOMServer.renderToStaticMarkup(
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-g4xw-jxrg-5f6mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-0969ghsaADVISORY
- discuss.hashicorp.com/t/hcsec-2026-01-arbitrary-code-execution-in-react-server-side-rendering-of-untrusted-mdx-content/77155nvdWEB
- github.com/hashicorp/next-mdx-remote/commit/4d527fdcaed911b87f427d0b4d3c711e817fa4b3ghsaWEB
- github.com/hashicorp/next-mdx-remote/releases/tag/v6.0.0ghsaWEB
News mentions
0No linked articles in our index yet.