{"record":{"id":"fceca4bd52ae76aa","repo":"mastra-ai/mastra","slug":"invalid-resourceid-resourceid","errorCode":null,"errorMessage":"Invalid resourceId: ${resourceId}","messagePattern":"Invalid resourceId: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/sdk/src/utils/plans.ts","lineNumber":86,"sourceCode":"  const rel = path.relative(plansDir, abs);\n  // Must be directly inside the plans dir (no nested subdirectories, no escaping it).\n  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return false;\n  return !rel.includes(path.sep);\n}\n\nexport async function savePlanToDisk(opts: {\n  title: string;\n  plan: string;\n  resourceId: string;\n  plansDir?: string;\n}): Promise<void> {\n  const { title, plan, resourceId } = opts;\n  const plansDir = opts.plansDir ?? getPlansDir();\n  const baseDir = path.resolve(plansDir);\n  const dir = path.resolve(baseDir, resourceId);\n  const rel = path.relative(baseDir, dir);\n  if (rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new Error(`Invalid resourceId: ${resourceId}`);\n  }\n\n  await fs.mkdir(dir, { recursive: true });\n\n  const now = new Date();\n  const timestamp = now.toISOString().replace(/:/g, '-');\n  const slug = slugify(title);\n  const filename = `${timestamp}-${slug}.md`;\n\n  const content = `# ${title}\\n\\nApproved: ${now.toISOString()}\\n\\n${plan}\\n`;\n\n  await fs.writeFile(path.join(dir, filename), content, 'utf-8');\n}\n\n/**\n * Read a plan markdown file by absolute path.\n *\n * The leading `# <title>` heading (if present) is parsed as the title and the remaining","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/utils/plans.ts#L68-L104","documentation":"savePlanToDisk writes an approved plan under <plansDir>/<resourceId>/ and defends against path traversal: it resolves the resourceId against the base plans directory and rejects any resourceId that escapes that directory (relative path containing '..' segments or an absolute path). The library throws 'Invalid resourceId: <id>' when the resourceId resolves outside the plans directory, preventing files from being written to arbitrary filesystem locations.","triggerScenarios":"Calling savePlanToDisk (or approvePlanFile which delegates to it) with a resourceId that is an absolute path (e.g. '/etc/foo'), contains '..' segments (e.g. '../../escape'), or on Windows-like absolute forms, such that path.resolve(plansDir, resourceId) lands outside plansDir.","commonSituations":"Passing an un-sanitized resourceId derived from user or LLM input (e.g. an API key, thread id, or file path) into approvePlanFile; misusing the resourceId parameter as a target file path; concatenating paths before calling the API; resourceId strings produced from URL or CLI input that include slashes or dot-dot segments.","solutions":["Sanitize the resourceId before calling: strip or reject path separators and '..' segments (e.g. resourceId.replace(/[^a-zA-Z0-9_-]/g, '_') or a slugify call).","Validate with the same check the library uses: ensure path.relative(path.resolve(plansDir), path.resolve(plansDir, resourceId)) does not start with '..' and is not absolute.","Pass a plain identifier (a slug or uuid) as resourceId, not a path; if you need a nested path, that is unsupported — use the default single-level layout.","If resourceId comes from external input, validate/normalize it at the boundary (approvePlanFile caller) and throw your own descriptive error early."],"exampleFix":"// before\nawait savePlanToDisk({ title, plan, resourceId: userInput }); // e.g. '../../etc/evil'\n// after\nconst safeId = userInput.replace(/[^a-zA-Z0-9_-]/g, '_');\nconst rel = path.relative(path.resolve(plansDir), path.resolve(plansDir, safeId));\nif (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`Invalid resourceId: ${safeId}`);\nawait savePlanToDisk({ title, plan, resourceId: safeId, plansDir });","handlingStrategy":"validation","validationCode":"import path from 'node:path';\nfunction assertSafeResourceId(resourceId, plansDir) {\n  const baseDir = path.resolve(plansDir);\n  const rel = path.relative(baseDir, path.resolve(baseDir, resourceId));\n  if (rel.startsWith('..') || path.isAbsolute(rel) || rel === '') {\n    throw new Error(`resourceId must be a single safe directory name, got: ${resourceId}`);\n  }\n  return resourceId;\n}","typeGuard":"function isSafeResourceId(resourceId, plansDir) {\n  const baseDir = path.resolve(plansDir);\n  const rel = path.relative(baseDir, path.resolve(baseDir, resourceId));\n  return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);\n}","tryCatchPattern":"try {\n  await savePlanToDisk({ title, plan, resourceId, plansDir });\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Invalid resourceId')) {\n    // sanitize resourceId and retry\n    await savePlanToDisk({ title, plan, resourceId: resourceId.replace(/[^a-zA-Z0-9_-]/g, '_'), plansDir });\n  } else throw err;\n}","preventionTips":["Treat resourceId as an identifier, never a path; slugify or UUID it at the source.","Never pass user/LLM-supplied strings with '/', '\\\\', or '..' directly as resourceId.","Run the path.relative containment check yourself before calling approvePlanFile/savePlanToDisk.","Log and reject unsafe ids at your API boundary with a descriptive 4xx rather than letting the library throw."],"tags":["path-traversal","validation","filesystem","security"],"backgroundTag":"path-traversal-rejected","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}