{"record":{"id":"9a32308ce164789d","repo":"vercel-labs/skills","slug":"invalid-subpath-subpath-resolves-outside-the","errorCode":null,"errorMessage":"Invalid subpath: \"${subpath}\" resolves outside the repository directory. Subpath must not contain \"..\" segments that escape the base path.","messagePattern":"Invalid subpath: \"(.+?)\" resolves outside the repository directory\\. Subpath must not contain \"\\.\\.\" segments that escape the base path\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/skills.ts","lineNumber":189,"sourceCode":"  const normalizedTarget = normalize(resolve(join(basePath, subpath)));\n\n  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;\n}\n\nexport async function discoverSkills(\n  basePath: string,\n  subpath?: string,\n  options?: DiscoverSkillsOptions\n): Promise<Skill[]> {\n  const skills: Skill[] = [];\n  const seenNames = new Set<string>();\n  const parsedSkillPaths = new Set<string>();\n  const localLock = await readLocalLock(basePath);\n  const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));\n\n  // Validate subpath doesn't escape basePath (prevent path traversal)\n  if (subpath && !isSubpathSafe(basePath, subpath)) {\n    throw new Error(\n      `Invalid subpath: \"${subpath}\" resolves outside the repository directory. Subpath must not contain \"..\" segments that escape the base path.`\n    );\n  }\n\n  const searchPath = subpath ? join(basePath, subpath) : basePath;\n\n  // Get plugin groupings to map skills to their parent plugin\n  // We search for plugin definitions from the base search path\n  const pluginGroupings = await getPluginGroupings(searchPath);\n\n  // Helper to assign plugin name if available\n  const enhanceSkill = (skill: Skill) => {\n    const resolvedPath = resolve(skill.path);\n    if (pluginGroupings.has(resolvedPath)) {\n      skill.pluginName = pluginGroupings.get(resolvedPath);\n    }\n    return skill;\n  };","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/vercel-labs/skills/blob/435076e78988e1e6ec40d00b0b1d76bdbbc5419a/src/skills.ts#L171-L207","documentation":"discoverSkills() validates any provided subpath with isSubpathSafe(basePath, subpath) before searching; if the subpath resolves outside basePath (contains '..' segments that escape), it throws this traversal error. This guards skill discovery inside cloned repos against path injection via the source spec's subpath component.","triggerScenarios":"Calling discoverSkills(basePath, subpath) (ultimately from 'skills add owner/repo/sub..') with a subpath like '../../', 'a/../../..', or one that normalizes outside the repo root.","commonSituations":"User-supplied subpaths from CLI args or SDK strings like 'skills/../../etc'; URL parsing quirks that keep '..' segments; programmatic callers joining unvalidated user input into the subpath parameter.","solutions":["Strip '..' segments from the subpath before calling discoverSkills (or sanitizeSubpath from source-parser)","Validate user input: allow only [A-Za-z0-9._/-] and reject any '..' component","Pass an absolute directory instead of a traversal-prone relative subpath","Fail fast in your CLI/SDK on suspicious path input rather than forwarding it"],"exampleFix":"// before\ndiscoverSkills(repoDir, userInput); // userInput = '../../secrets'\n// after\nif (userInput.split(/[\\\\/]/).includes('..')) throw new Error('bad subpath');\ndiscoverSkills(repoDir, userInput);","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from 'node:path';\nfunction isSubpathSafe(base: string, sub: string): boolean {\n  if (sub.includes('..')) return false;\n  const target = resolve(base, sub);\n  const rel = relative(resolve(base), target);\n  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n}\nif (subpath && !isSubpathSafe(basePath, subpath)) throw new Error('Unsafe subpath rejected');","typeGuard":"function isSubpathTraversal(e: unknown): e is Error {\n  return e instanceof Error && /Invalid subpath.*outside the repository directory/.test(e.message);\n}","tryCatchPattern":"try { await discoverSkills(basePath, subpath); }\ncatch (e) {\n  if (isSubpathTraversal(e)) throw new Error(`Rejected unsafe subpath: ${JSON.stringify(subpath)}`);\n  throw e;\n}","preventionTips":["Never forward raw user input as a subpath — whitelist characters","Reject any subpath containing '..' before calling discovery APIs","Prefer absolute validated paths over composed relative subpaths"],"tags":["security","path-traversal","subpath","skill-discovery"],"backgroundTag":"path-traversal-validation","analyzedSha":"435076e78988e1e6ec40d00b0b1d76bdbbc5419a","analyzedAt":"2026-08-28T17:47:53.369Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}