{"record":{"id":"db70d34545b04f9b","repo":"nocobase/nocobase","slug":"path-traversal-detected","errorCode":null,"errorMessage":"Path traversal detected","messagePattern":"Path traversal detected","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/core/server/src/plugin-manager/utils.ts","lineNumber":71,"sourceCode":"    throw new Error('Invalid plugin package name');\n  }\n\n  if (packageName.includes('..') || packageName.includes('\\\\')) {\n    throw new Error('Invalid plugin package name');\n  }\n\n  const valid = /^(?:@[a-z0-9][a-z0-9._-]*\\/)?[a-z0-9][a-z0-9._-]*$/i.test(packageName);\n  if (!valid) {\n    throw new Error('Invalid plugin package name');\n  }\n}\n\nexport function resolveSafeChildPath(baseDir: string, child: string) {\n  const resolvedBase = path.resolve(baseDir);\n  const resolvedTarget = path.resolve(baseDir, child);\n\n  if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(`${resolvedBase}${path.sep}`)) {\n    throw new Error('Path traversal detected');\n  }\n\n  return resolvedTarget;\n}\n\nexport function getLocalPluginPackagesPathArr(): string[] {\n  const pluginPackagesPathArr = process.env.PLUGIN_PATH || DEFAULT_PLUGIN_PATH;\n  return pluginPackagesPathArr.split(',').map((pluginPackagesPath) => {\n    pluginPackagesPath = pluginPackagesPath.trim();\n    return path.isAbsolute(pluginPackagesPath) ? pluginPackagesPath : path.join(process.cwd(), pluginPackagesPath);\n  });\n}\n\nexport function getStoragePluginDir(packageName: string) {\n  const pluginStoragePath = resolvePluginStoragePath();\n  assertSafePluginPackageName(packageName);\n  return resolveSafeChildPath(pluginStoragePath, packageName);\n}","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/nocobase/nocobase/blob/fa42722fefe44265490dff2c27d79e2882bce4fa/packages/core/server/src/plugin-manager/utils.ts#L53-L89","documentation":"resolveSafeChildPath resolves baseDir + child and verifies the result stays inside baseDir. If the resolved target escapes the base (via '..' segments, absolute child, or symlinks resolving outside), it throws 'Path traversal detected'. getStoragePluginDir and getNodeModulesPluginDir use it to sandbox plugin package names.","triggerScenarios":"getStoragePluginDir('..') or getNodeModulesPluginDir('../other') where the resolved path escapes the storage/NODE_MODULES base; an absolute child path pointing outside the base; NODE_MODULES_PATH or the plugin storage root misconfigured such that the resolved path falls outside.","commonSituations":"Malicious or buggy inputs with '../' sequences; env vars like NODE_MODULES_PATH pointing to unexpected locations so even valid names resolve outside; case-sensitive filesystem mismatches on macOS/Windows; code passing absolute paths as the child argument.","solutions":["Remove any '..' or absolute-path components from the child argument; pass a bare package name.","Verify the relevant env vars (NODE_MODULES_PATH, plugin storage path, PLUGIN_PATH) point to the intended real directories (check with path.resolve/realpath).","Log resolvedBase and resolvedTarget to see exactly how the escape occurs.","If intentionally referencing another directory, do not use this API — it is a security boundary by design."],"exampleFix":"// before\nconst dir = getStoragePluginDir('../../etc');\n// after\nconst name = '@my/safe-plugin'; // valid, non-traversing package name\nconst dir = getStoragePluginDir(name);","handlingStrategy":"validation","validationCode":"const path = require('path');\nfunction staysInsideBase(baseDir, child) {\n  const base = path.resolve(baseDir);\n  const target = path.resolve(baseDir, child);\n  return target === base || target.startsWith(base + path.sep);\n}\nif (!staysInsideBase(storagePath, name)) throw new Error('Child path escapes base: ' + child);","typeGuard":"function isSafeRelativeName(v: string): boolean {\n  return typeof v === 'string' && !v.includes('..') && !v.includes('\\\\') && !v.includes('\\0') && !require('path').isAbsolute(v);\n}","tryCatchPattern":"try {\n  const dir = getStoragePluginDir(name);\n} catch (e) {\n  if (e.message === 'Path traversal detected') {\n    console.error(`Refusing path escape for '${name}'; base=${storagePath}`);\n    return;\n  }\n  throw e;\n}","preventionTips":["Only pass bare package names (no '..', no absolute paths) into storage path helpers","Keep NODE_MODULES_PATH / storage env vars pointing at real directories verified with fs.realpath","Treat this error as a security signal: log and audit the source of the offending input","Never bypass resolveSafeChildPath with raw path.join for plugin storage"],"tags":["security","path-traversal","filesystem"],"backgroundTag":"path-traversal","analyzedSha":"fa42722fefe44265490dff2c27d79e2882bce4fa","analyzedAt":"2026-09-01T00:54:31.202Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}