{"record":{"id":"cc17720444a2bdfa","repo":"upstash/context7","slug":"skill-file-path-file-path-resolves-outside-th","errorCode":null,"errorMessage":"Skill file path \"${file.path}\" resolves outside the target directory","messagePattern":"Skill file path \"(.+?)\" resolves outside the target directory","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/cli/src/utils/installer.ts","lineNumber":23,"sourceCode":"import { assertSkillNameInRoot } from \"./skill-name.js\";\n\nexport async function installSkillFiles(\n  skillName: string,\n  files: SkillFile[],\n  skillsRoot: string\n): Promise<void> {\n  const skillDir = assertSkillNameInRoot(skillsRoot, skillName);\n\n  for (const file of files) {\n    const filePath = resolve(skillDir, file.path);\n\n    // Prevent directory traversal — resolved path must stay within skillDir\n    if (\n      !filePath.startsWith(skillDir + \"/\") &&\n      !filePath.startsWith(skillDir + \"\\\\\") &&\n      filePath !== skillDir\n    ) {\n      throw new Error(`Skill file path \"${file.path}\" resolves outside the target directory`);\n    }\n\n    const fileDir = dirname(filePath);\n\n    await mkdir(fileDir, { recursive: true });\n    await writeFile(filePath, file.content);\n  }\n}\n\nexport async function symlinkSkill(\n  skillName: string,\n  sourcePath: string,\n  skillsRoot: string\n): Promise<void> {\n  const targetPath = assertSkillNameInRoot(skillsRoot, skillName);\n\n  try {\n    const stats = await lstat(targetPath);","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/upstash/context7/blob/ca15df0443ee770506fc4eb270d1efc71d483933/packages/cli/src/utils/installer.ts#L5-L41","documentation":"A path-traversal guard inside installSkillFiles(). For each downloaded file, the resolved absolute path is checked to ensure it stays inside skillDir; if a file.path uses '../', an absolute path, or a Windows drive that escapes, the install aborts before any writeFile. This is a security boundary defending against malicious or compromised skill bundles.","triggerScenarios":"A downloaded skill manifest contains a file entry like '../../.bashrc', an absolute '/etc/...', a Windows 'C:\\\\...' path, or a symlink-style relative escape; the check fires before mkdir/writeFile so nothing is written.","commonSituations":"Skill author included absolute paths by mistake; a third-party/compromised skill tries to write outside its directory; path separator confusion on Windows where startsWith(skillDir + '\\\\') is the matching branch; bundled skill packaged with a build tool that emitted rooted paths.","solutions":["Inspect the skill bundle's file list for absolute or '../' entries and report it to the skill author.","If you maintain the skill, keep all file paths relative and contained within the skill folder.","Do not disable this guard — it is a security control; instead fix the offending path.","Confirm the skillsRoot and skillName resolve as expected (no unexpected symlink in the parent chain)."],"exampleFix":"// before — manifest contains an absolute path and trips the guard\nfiles: [{ path: \"/etc/evil\", content: \"...\" }]\n\n// after — keep every path relative under the skill folder\nfiles: [{ path: \"SKILL.md\", content: \"...\" }, { path: \"scripts/run.sh\", content: \"...\" }]","handlingStrategy":"validation","validationCode":"// Reject escaping paths before calling installSkillFiles.\nimport { resolve, relative } from \"node:path\";\nfunction isContained(files: { path: string }[], skillDir: string): boolean {\n  const root = resolve(skillDir);\n  return files.every((f) => {\n    const rel = relative(root, resolve(root, f.path));\n    return (rel === \"\" || !rel.startsWith(\"..\")) && !resolve(root, f.path).includes(\"\\\\0\");\n  });\n}\nif (!isContained(downloadData.files, skillDir)) {\n  throw new Error(\"Refusing to install skill: one or more file paths escape the skill directory.\");\n}","typeGuard":"function isSafeRelativePath(skillDir: string, p: string): boolean {\n  if (typeof p !== \"string\" || p.length === 0 || p.includes(\"\\0\")) return false;\n  const rel = relative(resolve(skillDir), resolve(skillDir, p));\n  return rel === \"\" || (!rel.startsWith(\"..\") && !resolve(skillDir, p).startsWith(\"/\"));\n}","tryCatchPattern":"try {\n  await installSkillFiles(name, files, skillDir);\n} catch (e) {\n  if (/resolves outside the target directory/i.test((e as Error).message)) {\n    // Security-relevant: do NOT retry; report the offending bundle upstream.\n    throw new Error(`Skill \"${name}\" contains an escaping path and was blocked. Report to the skill author.`);\n  }\n  throw e;\n}","preventionTips":["Treat this guard as a security control — never bypass it; fix the offending path instead.","When authoring skills, run file paths through a normalizer that strips leading slashes and rejects '../'.","Audit third-party skills before install by listing their file entries."],"tags":["security","path-traversal","skill-install","validation"],"backgroundTag":null,"analyzedSha":"ca15df0443ee770506fc4eb270d1efc71d483933","analyzedAt":"2026-08-12T13:31:48.440Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}