{"record":{"id":"b6e05a4b59baa18d","repo":"nanocoai/nanoclaw","slug":"cwd-escapes-the-plugin-root","errorCode":null,"errorMessage":"cwd escapes the plugin root","messagePattern":"cwd escapes the plugin root","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/container-config.ts","lineNumber":227,"sourceCode":"  }\n  return record;\n}\n\n/** Accept only the spec's fixed cwd shapes, lexically contained (no \"..\" segments). */\nfunction parseCwd(value: unknown): string | undefined {\n  if (value === undefined) return undefined;\n  if (typeof value !== 'string' || !CWD_FORM_RE.test(value)) {\n    throw new Error('cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path]');\n  }\n  // rest === '' is the bare form (`${PLUGIN_DATA}`, `./`); empty segments in a\n  // non-empty rest are rejected for symmetry with the command validator.\n  const rest = value.startsWith('./') ? value.slice(2) : value.replace(CWD_FORM_RE, '');\n  if (\n    rest.includes('${') ||\n    rest.includes('\\\\') ||\n    (rest !== '' && rest.split('/').some((s) => s === '..' || s === ''))\n  ) {\n    throw new Error('cwd escapes the plugin root');\n  }\n  return value;\n}\n\nexport interface AdditionalMountConfig {\n  hostPath: string;\n  containerPath: string;\n  readonly?: boolean;\n}\n\n/** Shape of the materialized `container.json` file read by the container runner. */\nexport interface ContainerConfig {\n  mcpServers: Record<string, McpServerConfig>;\n  packages: { apt: string[]; npm: string[] };\n  imageTag?: string;\n  additionalMounts: AdditionalMountConfig[];\n  skills: string[] | 'all';\n  provider?: string;","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/container-config.ts#L209-L245","documentation":"parseCwd's containment check (container-config.ts:227): after matching the allowed prefix shape, the remaining path contains `..` segments, `${`, a backslash, or empty segments (e.g. \"a//b\") — i.e. it could escape the plugin root. This is a lexical security guard, not a filesystem check.","triggerScenarios":"cwd values like \"./a/../../etc\", \"${PLUGIN_ROOT}/../x\", \"./a\\\\b\", or \"./a//b\" in a stdio MCP entry.","commonSituations":"Path traversal attempts or careless ../ joins in generated configs; Windows-style separators; double slashes from naive path concatenation.","solutions":["Remove `..` segments; stay within the plugin root/data dir","Use forward slashes only (no backslashes)","Build subpaths by appending clean segments, then sanity-check with a regex before saving"],"exampleFix":"// before\n{\"cwd\":\"${PLUGIN_ROOT}/../shared\"}\n// after\n{\"cwd\":\"${PLUGIN_ROOT}/shared-links\"}","handlingStrategy":"validation","validationCode":"const rest = cwd.replace(/^\\.\\//, '').replace(/^\\$\\{PLUGIN_(ROOT|DATA)\\}/, '');\nif (rest.includes('..') || rest.includes('\\\\') || rest.includes('${') || rest.split('/').some(s => s === '')) throw new UserError('cwd escapes plugin root');","typeGuard":"const isContainedCwd = (v: string) => { const r = v.replace(/^\\.\\//, '').replace(/^\\$\\{PLUGIN_(ROOT|DATA)\\}/, ''); return !r.includes('..') && !r.includes('\\\\') && !r.includes('${') && (r === '' || r.split('/').every(s => s !== '')); };","tryCatchPattern":"catch (err) { if (err.message.includes('escapes the plugin root')) rebuildPathWithoutTraversal(); else throw err; }","preventionTips":["Join paths programmatically instead of string concatenation with ../","Reject any config containing '..' segments before persisting"],"tags":["mcp","path-traversal","security-policy","config-validation"],"backgroundTag":"path-traversal-rejected","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}