{"record":{"id":"b3b65b613e476202","repo":"mastra-ai/mastra","slug":"skillpath-must-be-within-the-allowed-directory","errorCode":null,"errorMessage":"skillPath must be within the allowed directory: ${allowedBase}","messagePattern":"skillPath must be within the allowed directory: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"packages/server/src/server/handlers/stored-skills.ts","lineNumber":615,"sourceCode":"      }\n      assertStoredResourceScope(existing, await getStoredResourceScope(mastra, requestContext));\n\n      // Throws 404 if the caller isn't the owner, admin, or `stored-skills:write[:<id>]` holder.\n      assertWriteAccess({\n        requestContext,\n        resource: 'stored-skills',\n        resourceId: storedSkillId,\n        action: 'edit',\n        record: existing,\n      });\n\n      // Validate skillPath to prevent path traversal\n      const path = await import('node:path');\n      const fs = await import('node:fs/promises');\n      const resolvedPath = path.default.resolve(skillPath);\n      const allowedBase = path.default.resolve(process.env.SKILLS_BASE_DIR || process.cwd());\n      if (!resolvedPath.startsWith(allowedBase + path.default.sep) && resolvedPath !== allowedBase) {\n        throw new HTTPException(400, {\n          message: `skillPath must be within the allowed directory: ${allowedBase}`,\n        });\n      }\n\n      // Verify the source directory exists and contains a SKILL.md before attempting\n      // to publish, so callers get a 400 with context instead of a raw 500/ENOENT.\n      try {\n        const stat = await fs.stat(resolvedPath);\n        if (!stat.isDirectory()) {\n          throw new HTTPException(400, { message: `skillPath is not a directory: ${resolvedPath}` });\n        }\n      } catch (err) {\n        if (err instanceof HTTPException) throw err;\n        if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {\n          throw new HTTPException(400, {\n            message: `skillPath does not exist on the server filesystem: ${resolvedPath}. Create the skill directory (with a SKILL.md) before publishing, or use a skill that was materialized to disk.`,\n          });\n        }","sourceCodeStart":597,"sourceCodeEnd":633,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/stored-skills.ts#L597-L633","documentation":"A 400 thrown to prevent path traversal: the handler resolves `skillPath` to an absolute path and rejects it unless it lies under `SKILLS_BASE_DIR` (or the server's cwd when unset). Publishing skills must only read directories the server operator allows.","triggerScenarios":"Passing an absolute path outside the allowed base (e.g. `/etc`, `/home/user/other-project`), or a relative path with `../` segments that resolves outside the base, in the publish-skill request.","commonSituations":"Developer passes their local skill folder path to a remote server where the base dir differs; SKILLS_BASE_DIR unset so the base is the server's cwd, not the expected skills root; symlinks/relative paths resolving unexpectedly.","solutions":["Set `SKILLS_BASE_DIR` on the server to the directory containing publishable skills and place the skill folder under it","Pass a path inside the allowed base (or a relative path that resolves under it)","Compare the resolved path against `path.resolve(process.env.SKILLS_BASE_DIR || process.cwd())` locally before calling","Avoid `..` segments and cross-directory references in skillPath"],"exampleFix":"// before\nawait publishSkill({ skillPath: '/Users/me/skills/my-skill' });\n// after\nawait publishSkill({ skillPath: '/srv/skills/my-skill' }); // server: SKILLS_BASE_DIR=/srv/skills","handlingStrategy":"validation","validationCode":"import path from 'node:path';\nconst allowedBase = path.resolve(process.env.SKILLS_BASE_DIR || process.cwd());\nconst resolved = path.resolve(skillPath);\nif (!resolved.startsWith(allowedBase + path.sep) && resolved !== allowedBase) {\n  throw new Error(`skillPath must be inside ${allowedBase}`);\n}","typeGuard":null,"tryCatchPattern":"try { await publishStoredSkill({ skillPath }); } catch (e) {\n  if (e.status === 400 && /allowed directory/.test(e.message)) throw new Error(`Move the skill under the server's allowed base: ${e.message}`);\n  throw e;\n}","preventionTips":["Set SKILLS_BASE_DIR explicitly on the server and keep skills under it","Always resolve paths (path.resolve) before comparing — never string-prefix raw input","Reject or normalize `..` segments early in client code","Remember paths are evaluated on the SERVER, not the caller's machine"],"tags":["security","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}