{"record":{"id":"10fa15d5366bb808","repo":"upstash/context7","slug":"skill-name-skillname-escapes-the-skills-root","errorCode":null,"errorMessage":"Skill name \"${skillName}\" escapes the skills root","messagePattern":"Skill name \"(.+?)\" escapes the skills root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/cli/src/utils/skill-name.ts","lineNumber":21,"sourceCode":"const SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;\n\nexport function isSafeSkillName(name: string): boolean {\n  if (typeof name !== \"string\") return false;\n  if (name.length === 0 || name.length > 128) return false;\n  if (name === \".\" || name === \"..\") return false;\n  if (name.includes(\"\\0\")) return false;\n  if (!SAFE_NAME.test(name)) return false;\n  return true;\n}\n\nexport function assertSkillNameInRoot(skillsRoot: string, skillName: string): string {\n  if (!isSafeSkillName(skillName)) {\n    throw new Error(`Unsafe skill name: ${JSON.stringify(skillName)}`);\n  }\n  const root = resolve(skillsRoot);\n  const target = resolve(root, skillName);\n  if (dirname(target) !== root || basename(target) !== skillName) {\n    throw new Error(`Skill name \"${skillName}\" escapes the skills root`);\n  }\n  return target;\n}\n","sourceCodeStart":3,"sourceCodeEnd":25,"githubUrl":"https://github.com/upstash/context7/blob/4416fb855b8f752be735e34f943b5d0762701aad/packages/cli/src/utils/skill-name.ts#L3-L25","documentation":"assertSkillNameInRoot validates a skill name before resolving it under the skills root directory. After checking the name against isSafeSkillName, it resolves the joined path and verifies the resolved directory is still the skills root and the basename equals the original name. If a crafted name (e.g. containing '..' or separators) resolves outside the root, it throws to block path traversal.","triggerScenarios":"Calling skillDir/targetPath/removeCommand with a skill name like '../other', 'a/b', an absolute path, or any string containing path separators or '..' segments, so that resolve(root, skillName) lands outside the skills root.","commonSituations":"User-supplied skill names passed to CLI remove/skill commands without sanitization; automation scripts interpolating paths into skill names; names copied from URLs or file paths; locale/encoding oddities introducing separator characters.","solutions":["Sanitize the skill name before calling: strip path separators and '..' segments, or reject names not matching /^[A-Za-z0-9._-]+$/","Ensure the name is a plain single path segment (no '/' or '\\\\', not '..')","If the intent was a nested path, pass only the final directory name and manage nesting inside the library","Log/inspect the rejected name to find where unsanitized input enters the CLI"],"exampleFix":"// before\nremoveCommand(`../${userInput}`);\n// after\nif (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(userInput)) throw new Error('invalid skill name');\nremoveCommand(userInput);","handlingStrategy":"validation","validationCode":"const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nfunction isValidSkillName(name) { return typeof name === 'string' && SAFE_SKILL_NAME.test(name) && name !== '..' && name !== '.'; }","typeGuard":"function isSafeSkillName(name: unknown): name is string {\n  return typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);\n}","tryCatchPattern":"try {\n  const dir = skillDir(userInput);\n} catch (e) {\n  if (e.message.includes('escapes the skills root') || e.message.startsWith('Unsafe skill name')) {\n    throw new UserFacingError(`Invalid skill name: ${userInput}`);\n  }\n  throw e;\n}","preventionTips":["Always validate user-supplied names against a strict allowlist pattern before passing them to skill APIs","Never build skill names from file paths, URLs, or user raw input without sanitization","Treat path traversal rejections as a security signal and log the attempted input","Keep the library's assertSkillNameInRoot call in place; do not bypass it"],"tags":["security","path-traversal","validation","cli"],"backgroundTag":"path-traversal-blocked","analyzedSha":"4416fb855b8f752be735e34f943b5d0762701aad","analyzedAt":"2026-09-16T20:28:07.148Z","contentChangedAt":"2026-09-16T20:28:07.148Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}