{"record":{"id":"4e092c931b1a1820","repo":"upstash/context7","slug":"skipping-file-with-unsafe-path-item-path","errorCode":null,"errorMessage":"Skipping file with unsafe path: ${item.path}","messagePattern":"Skipping file with unsafe path: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/cli/src/utils/github.ts","lineNumber":295,"sourceCode":"    return { files: [], error: `No files found in ${skillPath}` };\n  }\n\n  const files: SkillFile[] = [];\n  for (const item of skillFiles) {\n    const rawUrl = `${GITHUB_RAW}/${owner}/${repo}/${branch}/${item.path}`;\n    const fileResponse = await fetch(rawUrl, { headers: ghHeaders });\n\n    if (!fileResponse.ok) {\n      console.warn(`Failed to fetch ${item.path}: ${fileResponse.status}`);\n      continue;\n    }\n\n    const content = await fileResponse.text();\n    const relativePath = item.path.slice(skillPath.length + 1);\n\n    // Reject paths that attempt directory traversal\n    if (relativePath.includes(\"..\")) {\n      console.warn(`Skipping file with unsafe path: ${item.path}`);\n      continue;\n    }\n\n    files.push({\n      path: relativePath,\n      content,\n    });\n  }\n\n  return { files };\n}\n\nasync function downloadSingleSkillFile(\n  skillUrl: string,\n  ghHeaders: Record<string, string>\n): Promise<SkillFile[] | null> {\n  let fileName: string;\n  try {","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/upstash/context7/blob/5284672feb575908efead6fcf1b5e542f8d607bb/packages/cli/src/utils/github.ts#L277-L313","documentation":"Defensive path-traversal guard in the GitHub file downloader: after stripping the skillPath prefix from each fetched file's path, the resulting relativePath is rejected if it contains '..'. The file is skipped with a console.warn rather than written, preventing a malicious or corrupted repo tree entry from writing outside the destination skills directory (e.g. ../../.bashrc). Git itself normally forbids '..' in tree paths, so seeing this warn usually means the API response was tampered with, a proxy mangled it, or the skillPath prefix no longer matches item.path after upstream restructure.","triggerScenarios":"Installing a skill from a repo whose GitHub tree API response contains crafted paths escaping the skills folder; an intercepting corporate proxy rewriting response bodies; a CLI/skillPath version mismatch where slice() produces a mangled relative path.","commonSituations":"Installing skills from untrusted or hijacked repos; security tooling fuzzing the installer; virtually never seen with legitimate upstream repos — treat it as a red flag about the repo or the transport.","solutions":["Open the repo's skills folder in a browser and eyeball the file list for odd ../ style paths or unexpected files","Do not install skills from the offending repo; report it if it came from a public listing","Update the CLI so the path-prefix logic matches the current API shape","If behind an intercepting proxy, compare the raw API response with and without the proxy"],"exampleFix":"// before\nconst relativePath = item.path.slice(skillPath.length + 1);\nif (relativePath.includes(\"..\")) {\n  console.warn(`Skipping file with unsafe path: ${item.path}`);\n  continue;\n}\n\n// after — strict segment check + containment assert\nconst relativePath = item.path.slice(skillPath.length + 1);\nconst isSafe = relativePath.split(\"/\").every((seg) => seg !== \"..\" && seg.length > 0);\nconst dest = resolve(targetDir, relativePath);\nif (!isSafe || !dest.startsWith(resolve(targetDir) + sep)) {\n  console.warn(`Skipping file with unsafe path: ${item.path}`);\n  continue;\n}","handlingStrategy":"type-guard","validationCode":"// Reject unsafe relative paths BEFORE any filesystem write\nfunction isSafeRelativePath(rel: string): boolean {\n  if (rel.includes(\"..\")) return false;\n  if (rel.startsWith(\"/\") || /^[a-zA-Z]:/.test(rel)) return false; // no absolute/Windows roots\n  return rel.split(\"/\").every((seg) => seg.length > 0 && seg !== \".\");\n}\nconst safe = isSafeRelativePath(relativePath);\nif (!safe) {\n  console.warn(`Skipping file with unsafe path: ${item.path}`);\n  continue;\n}","typeGuard":"function isSafeSkillFilePath(item: { path: string }, skillPath: string): boolean {\n  if (!item.path.startsWith(skillPath + \"/\")) return false; // must live under skillPath\n  const rel = item.path.slice(skillPath.length + 1);\n  const segments = rel.split(\"/\");\n  return segments.length > 0 && segments.every((s) => s !== \"..\" && s !== \".\" && s.length > 0);\n}","tryCatchPattern":null,"preventionTips":["Validate extracted/remote paths with a segment-based '..' check before resolving them onto disk","After resolve(), additionally assert dest.startsWith(resolve(targetDir) + sep) for defense in depth","Only install skills from repos you trust; treat this warn as evidence of tampering, not noise"],"tags":["security","path-traversal","github","filesystem","skills"],"backgroundTag":"path-traversal-blocked","analyzedSha":"5284672feb575908efead6fcf1b5e542f8d607bb","analyzedAt":"2026-08-18T18:00:18.510Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}