mjmlio/mjml · warning

[MJML security] includePath "${p}" is the root directory, wh

Error message

[MJML security] includePath "${p}" is the root directory, which is unsafe. Consider scoping includePath to a project templates folder.

What it means

mjml2html validates each entry in the includePath option: if a path exists and its realpath equals the filesystem root ('/'), it warns that allowing includes from the entire filesystem is unsafe. It still proceeds, but any mj-include could read any file the process can access, so the configuration is flagged as a security risk.

Source

Thrown at packages/mjml-core/src/index.js:596

  if (typeof mjml === 'string') {
    const pathsArr = []
    if (Array.isArray(includePath)) {
      pathsArr.push(
        ...includePath.filter((p) => typeof p === 'string' && p.length > 0),
      )
    } else if (includePath) {
      pathsArr.push(includePath)
    }

    if (pathsArr.length) {
      for (const p of pathsArr) {
        if (fs.existsSync(p)) {
          const r = fs.realpathSync(p)
          const isRootDir = r === path.parse(r).root
          if (isRootDir) {
            // eslint-disable-next-line no-console
            console.warn(
              `[MJML security] includePath "${p}" is the root directory, which is unsafe. Consider scoping includePath to a project templates folder.`,
            )
          }
        }
      }
    }

    mjml = MJMLParser(mjml, {
      keepComments,
      components,
      filePath,
      actualPath,
      preprocessors,
      ignoreIncludes,
      includePath,
    })
  }

View on GitHub (pinned to 6c01d35af5)

Solutions

  1. Replace the root entry with the specific templates folder, e.g. includePath: path.resolve(__dirname, 'templates')
  2. Add only the directories that actually contain included files
  3. If multiple locations are needed, list each explicit directory in the includePath array
  4. Avoid symlinks that resolve to '/' and never commit includePath: ['/'] to shared config

Example fix

// before
mjml2html({ mjml, allowIncludes: true, includePath: ['/'] })
// after
mjml2html({ mjml, allowIncludes: true, includePath: [path.join(__dirname, 'templates')] })
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs'; import path from 'path'
for (const p of includePath) {
  if (fs.existsSync(p) && fs.realpathSync(p) === path.parse(fs.realpathSync(p)).root) {
    throw new Error(`includePath ${p} is the filesystem root; scope it to a templates folder`)
  }
}

Type guard

const isScopedIncludePath = (p) => fs.existsSync(p) && fs.realpathSync(p) !== path.parse(fs.realpathSync(p)).root

Prevention

When it happens

Trigger: Calling mjml2html({ ..., includePath: ['/'] }) (or passing a path that symlinks/normalizes to root) with allowIncludes enabled. The check runs only when fs.existsSync(p) is true and fs.realpathSync(p) === path.parse(r).root.

Common situations: Setting includePath to '/' or '~' shortcuts to 'make every include work'; container images where '/' is the working root; copying example configs that used an overly broad path.

Related errors


AI-assisted analysis of mjmlio/mjml@6c01d35af5 (2026-09-02). Data as JSON: /api/errors/72c3d7662a688b00. Report an issue: GitHub.