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
- Replace the root entry with the specific templates folder, e.g. includePath: path.resolve(__dirname, 'templates')
- Add only the directories that actually contain included files
- If multiple locations are needed, list each explicit directory in the includePath array
- 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
- Never configure includePath as '/' or '~'
- Use path.resolve(__dirname, 'templates') for explicit scoping
- Review symlinked include paths that may resolve to root
- Keep includePath entries as narrowly scoped as possible
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
- [MJML] Some mj-include paths were denied because they are ou
- [MJML] mj-include tags were found but includes are disabled
- Mixed variable syntax detected. Use either CSS property synt
- Malformed MJML. Check that your structure is correct and enc
- No type found for ${typeConfig}
AI-assisted analysis of mjmlio/mjml@6c01d35af5 (2026-09-02).
Data as JSON: /api/errors/72c3d7662a688b00.
Report an issue: GitHub.