npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mdx-bundler-secure

v1.0.0

Published

Compile and bundle your MDX files and their dependencies. FAST. Security-hardened fork of mdx-bundler with RCE mitigation (CVE-2026-0969).

Downloads

98

Readme


version downloads MIT License

Why this fork?

CVE-2026-0969 disclosed a high-severity arbitrary code execution vulnerability in server-side MDX rendering. The original mdx-bundler is confirmed affected but has no patch. mdx-bundler-secure is a drop-in replacement that adds multi-layer RCE mitigation while keeping full backward compatibility.

What was the vulnerability?

MDX allows JavaScript expressions inside {curly braces}. When untrusted MDX is compiled and evaluated server-side, an attacker can inject:

{require('child_process').execSync('cat /etc/passwd')}
{eval("process.exit(1)")}
{new Function("return process.env")()}

These execute with full Node.js privileges on your server.

How does this fork fix it?

Three defence layers:

| Layer | What it does | Default | |---|---|---| | blockDangerousJS | Remark plugin blocks known dangerous patterns (eval, require, process, Function, import(), __proto__, etc.) and imports from Node.js built-in modules (child_process, fs, vm, ...) | true | | blockJS | Remark plugin strips all {expressions} from MDX before compilation | false | | Runtime shadow | getMDXExport shadows eval, Function, process, global, globalThis, __dirname, __filename as undefined in the new Function() scope | Always on |

Migrating from mdx-bundler

- npm install mdx-bundler
+ npm install mdx-bundler-secure
- import {bundleMDX} from 'mdx-bundler'
+ import {bundleMDX} from 'mdx-bundler-secure'

- import {getMDXComponent} from 'mdx-bundler/client'
+ import {getMDXComponent} from 'mdx-bundler-secure/client'

That's it. All existing MDX content with safe expressions ({title}, {frontmatter.date}, {items.map(...)}, etc.) works without changes.

Installation

npm install mdx-bundler-secure esbuild

One of mdx-bundler-secure's dependencies requires a working node-gyp setup to be able to install correctly.

Usage

import {bundleMDX} from 'mdx-bundler-secure'

const mdxSource = `
---
title: Example Post
published: 2021-02-13
description: This is some description
---

# Wahoo

import Demo from './demo'

Here's a **neat** demo:

<Demo />
`.trim()

const result = await bundleMDX({
  source: mdxSource,
  files: {
    './demo.tsx': `
import * as React from 'react'

function Demo() {
  return <div>Neat demo!</div>
}

export default Demo
    `,
  },
})

const {code, frontmatter} = result

From there, you send the code to your client, and then:

import * as React from 'react'
import {getMDXComponent} from 'mdx-bundler-secure/client'

function Post({code, frontmatter}) {
  const Component = React.useMemo(() => getMDXComponent(code), [code])
  return (
    <>
      <header>
        <h1>{frontmatter.title}</h1>
        <p>{frontmatter.description}</p>
      </header>
      <main>
        <Component />
      </main>
    </>
  )
}

Security Options

blockDangerousJS

Type: boolean | Default: true

Best-effort check that blocks known dangerous JavaScript patterns in MDX expressions and imports from dangerous Node.js built-in modules. Safe expressions like {title} or {items.map(x => x.name)} pass through.

Blocked patterns include: eval(), Function(), new Function(), require(), import(), process.*, global, globalThis, __dirname, __filename, __proto__, .constructor[, setTimeout("string", ...), Object.getPrototypeOf

Blocked modules include: child_process, fs, net, os, vm, worker_threads, http, https, and all node:* prefixed imports.

// Default: dangerous patterns are blocked
const result = await bundleMDX({source: mdxSource})

// Opt out for fully trusted content
const result = await bundleMDX({
  source: trustedMdxSource,
  blockDangerousJS: false,
})

blockJS

Type: boolean | Default: false

Strips all JavaScript expressions ({...}) from MDX before compilation. This is the strongest protection for rendering completely untrusted user-generated MDX.

const result = await bundleMDX({
  source: userSuppliedMdx,
  blockJS: true, // removes all {expressions}, only static markdown remains
})

When both blockJS and blockDangerousJS are true, blockJS takes precedence (strips all rather than throwing on dangerous ones).

Options

source

The string source of your MDX.

Can not be set if file is set

file

The path to the file on your disk with the MDX in. You will probably want to set cwd as well.

Can not be set if source is set

files

The files config is an object of all the files you're bundling. The key is the path to the file (relative to the MDX source) and the value is the string of the file source code. You could get these from the filesystem or from a remote database. If your MDX doesn't reference other files (or only imports things from node_modules), then you can omit this entirely.

mdxOptions

This allows you to modify the built-in MDX configuration (passed to @mdx-js/esbuild). This can be helpful for specifying your own remarkPlugins/rehypePlugins.

The function is passed the default mdxOptions and the frontmatter.

bundleMDX({
  source: mdxSource,
  mdxOptions(options, frontmatter) {
    options.remarkPlugins = [...(options.remarkPlugins ?? []), myRemarkPlugin]
    options.rehypePlugins = [...(options.rehypePlugins ?? []), myRehypePlugin]

    return options
  },
})

esbuildOptions

You can customize any of esbuild options with the option esbuildOptions. This takes a function which is passed the default esbuild options and the frontmatter and expects an options object to be returned.

bundleMDX({
  source: mdxSource,
  esbuildOptions(options, frontmatter) {
    options.minify = false
    options.target = [
      'es2020',
      'chrome58',
      'firefox57',
      'safari11',
      'edge16',
      'node12',
    ]

    return options
  },
})

More information on the available options can be found in the esbuild documentation.

globals

This tells esbuild that a given module is externally available. For example, if your MDX file uses the d3 library and you're already using the d3 library in your app then you'll end up shipping d3 to the user twice (once for your app and once for this MDX component). This is wasteful and you'd be better off just telling esbuild to not bundle d3 and you can pass it to the component yourself when you call getMDXComponent.

// server-side or build-time code that runs in Node:
import {bundleMDX} from 'mdx-bundler-secure'

const mdxSource = `
# This is the title

import leftPad from 'left-pad'

<div>{leftPad("Neat demo!", 12, '!')}</div>
`.trim()

const result = await bundleMDX({
  source: mdxSource,
  globals: {'left-pad': 'myLeftPad'},
})
// server-rendered and/or client-side code that can run in the browser or Node:
import * as React from 'react'
import leftPad from 'left-pad'
import {getMDXComponent} from 'mdx-bundler-secure/client'

function MDXPage({code}: {code: string}) {
  const Component = React.useMemo(
    () => getMDXComponent(result.code, {myLeftPad: leftPad}),
    [result.code, leftPad],
  )
  return (
    <main>
      <Component />
    </main>
  )
}

cwd

Setting cwd (current working directory) to a directory will allow esbuild to resolve imports. This directory could be the directory the mdx content was read from or a directory that off-disk mdx should be run in.

grayMatterOptions

This allows you to configure the gray-matter options.

bundleMDX({
  grayMatterOptions: options => {
    options.excerpt = true
    return options
  },
})

bundleDirectory & bundlePath

This allows you to set the output directory for the bundle and the public URL to the directory. If one option is set the other must be as well.

jsxConfig

Allows output for JSX runtimes other than React (Preact, Hono, Vue, etc). See Other JSX runtimes.

Returns

bundleMDX returns a promise for an object with the following properties.

Types

mdx-bundler-secure supplies complete typings within its own package.

bundleMDX has a single type parameter which is the type of your frontmatter. It defaults to {[key: string]: any} and must be an object.

const {frontmatter} = bundleMDX<{title: string}>({source})

frontmatter.title // has type string

Component Substitution

MDX Bundler passes on MDX's ability to substitute components through the components prop on the component returned by getMDXComponent.

import * as React from 'react'
import {getMDXComponent} from 'mdx-bundler-secure/client'

const Paragraph: React.FC = props => {
  if (typeof props.children !== 'string' && props.children.type === 'img') {
    return <>{props.children}</>
  }

  return <p {...props} />
}

function MDXPage({code}: {code: string}) {
  const Component = React.useMemo(() => getMDXComponent(code), [code])

  return (
    <main>
      <Component components={{p: Paragraph}} />
    </main>
  )
}

Frontmatter and const

You can reference frontmatter meta or consts in the mdx content.

---
title: Example Post
---

export const exampleImage = 'https://example.com/image.jpg'

# {frontmatter.title}

<img src={exampleImage} alt="Image alt text" />

Accessing named exports

You can use getMDXExport instead of getMDXComponent to treat the mdx file as a module instead of just a component. It takes the same arguments that getMDXComponent does.

import * as React from 'react'
import {getMDXExport} from 'mdx-bundler-secure/client'

function MDXPage({code}: {code: string}) {
  const mdxExport = getMDXExport(code)
  console.log(mdxExport.toc) // [ { depth: 1, value: 'The title' } ]

  const Component = React.useMemo(() => mdxExport.default, [code])

  return <Component />
}

Other JSX runtimes

JSX runtimes mentioned here are guaranteed to be supported. Any JSX runtime with its own jsx runtime export should work.

const getMDX = (source) => {
  return bundleMDX({
    source,
    jsxConfig: {
      jsxLib: {
        varName: 'HonoJSX',
        package: 'hono/jsx',
      },
      jsxDom: {
        varName: 'HonoDOM',
        package: 'hono/jsx/dom',
      },
      jsxRuntime: {
        varName: '_jsx_runtime',
        package: 'hono/jsx/jsx-runtime',
      },
    }
  });
}

// Client side
import { getMDXComponent } from "mdx-bundler-secure/client/jsx";
import * as HonoJSX from "hono/jsx";
import * as HonoDOM from "hono/jsx/dom";
import * as _jsx_runtime from "hono/jsx/jsx-runtime";

const Component = getMDXComponent(code, { HonoJSX, HonoDOM, _jsx_runtime });

Known Issues

Cloudflare Workers

Workers can't run binaries (esbuild) or eval/new Function. See the original repo for workarounds.

Next.JS esbuild ENOENT

esbuild relies on __dirname to find its executable. Adding the following before bundleMDX fixes this:

import path from 'path'

if (process.platform === 'win32') {
  process.env.ESBUILD_BINARY_PATH = path.join(
    process.cwd(), 'node_modules', 'esbuild', 'esbuild.exe',
  )
} else {
  process.env.ESBUILD_BINARY_PATH = path.join(
    process.cwd(), 'node_modules', 'esbuild', 'bin', 'esbuild',
  )
}

Credits

Original mdx-bundler by Kent C. Dodds and contributors.

LICENSE

MIT