{"record":{"id":"72c3d7662a688b00","repo":"mjmlio/mjml","slug":"mjml-security-includepath-p-is-the-root-dir","errorCode":null,"errorMessage":"[MJML security] includePath \"${p}\" is the root directory, which is unsafe. Consider scoping includePath to a project templates folder.","messagePattern":"\\[MJML security\\] includePath \"(.+?)\" is the root directory, which is unsafe\\. Consider scoping includePath to a project templates folder\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/mjml-core/src/index.js","lineNumber":596,"sourceCode":"\n  if (typeof mjml === 'string') {\n    const pathsArr = []\n    if (Array.isArray(includePath)) {\n      pathsArr.push(\n        ...includePath.filter((p) => typeof p === 'string' && p.length > 0),\n      )\n    } else if (includePath) {\n      pathsArr.push(includePath)\n    }\n\n    if (pathsArr.length) {\n      for (const p of pathsArr) {\n        if (fs.existsSync(p)) {\n          const r = fs.realpathSync(p)\n          const isRootDir = r === path.parse(r).root\n          if (isRootDir) {\n            // eslint-disable-next-line no-console\n            console.warn(\n              `[MJML security] includePath \"${p}\" is the root directory, which is unsafe. Consider scoping includePath to a project templates folder.`,\n            )\n          }\n        }\n      }\n    }\n\n    mjml = MJMLParser(mjml, {\n      keepComments,\n      components,\n      filePath,\n      actualPath,\n      preprocessors,\n      ignoreIncludes,\n      includePath,\n    })\n  }\n","sourceCodeStart":578,"sourceCodeEnd":614,"githubUrl":"https://github.com/mjmlio/mjml/blob/6c01d35af5da14108b86bbf052232e99de7ca755/packages/mjml-core/src/index.js#L578-L614","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nmjml2html({ mjml, allowIncludes: true, includePath: ['/'] })\n// after\nmjml2html({ mjml, allowIncludes: true, includePath: [path.join(__dirname, 'templates')] })","handlingStrategy":"validation","validationCode":"import fs from 'fs'; import path from 'path'\nfor (const p of includePath) {\n  if (fs.existsSync(p) && fs.realpathSync(p) === path.parse(fs.realpathSync(p)).root) {\n    throw new Error(`includePath ${p} is the filesystem root; scope it to a templates folder`)\n  }\n}","typeGuard":"const isScopedIncludePath = (p) => fs.existsSync(p) && fs.realpathSync(p) !== path.parse(fs.realpathSync(p)).root","tryCatchPattern":null,"preventionTips":["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"],"tags":["security","path-traversal","configuration","mjml","include-path"],"backgroundTag":"unsafe-root-include-path","analyzedSha":"6c01d35af5da14108b86bbf052232e99de7ca755","analyzedAt":"2026-09-02T21:31:03.649Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}