{"record":{"id":"777129ebcdbb89ae","repo":"can1357/oh-my-pi","slug":"path-traversal-is-not-allowed-in-skill-url","errorCode":null,"errorMessage":"Path traversal (..) is not allowed in skill:// URLs","messagePattern":"Path traversal \\(\\.\\.\\) is not allowed in skill:// URLs","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/skill-protocol.ts","lineNumber":40,"sourceCode":"\treturn \"text/plain\";\n}\n\n/**\n * Validate that a path is safe (no traversal, no absolute paths).\n */\nexport function validateRelativePath(relativePath: string): void {\n\tif (path.isAbsolute(relativePath)) {\n\t\tthrow new Error(\"Absolute paths are not allowed in skill:// URLs\");\n\t}\n\n\tconst normalized = path.normalize(relativePath);\n\tif (\n\t\trelativePath.split(/[\\\\/]/).includes(\"..\") ||\n\t\tnormalized.startsWith(\"..\") ||\n\t\tnormalized.includes(\"/../\") ||\n\t\tnormalized.includes(\"/..\")\n\t) {\n\t\tthrow new Error(\"Path traversal (..) is not allowed in skill:// URLs\");\n\t}\n}\n\n/**\n * Handler for skill:// URLs.\n */\nexport class SkillProtocolHandler implements ProtocolHandler {\n\treadonly scheme = \"skill\";\n\treadonly immutable = true;\n\n\tasync resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {\n\t\tconst skills = context?.skills ?? getActiveSkills();\n\n\t\tconst skillName = url.rawHost || url.hostname;\n\t\tif (!skillName) {\n\t\t\tthrow new Error(\"skill:// URL requires a skill name: skill://<name>\");\n\t\t}\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/skill-protocol.ts#L22-L58","documentation":"validateRelativePath() rejects any relative path containing '..' segments — as raw segments, after path.normalize(), or as prefixes/suffixes like '../x' or 'x/..'. This prevents directory traversal out of the skill's base directory when the path is joined and resolved. The check runs both on the split segments (before normalization) and the normalized string to defeat encoded or redundant-segment tricks.","triggerScenarios":"Resolving skill://<name>/../other-skill/file.md, skill://<name>/a/../../etc/passwd, or any decoded pathname whose segments include '..'; calling the exported validators (extractRelativePath, splitMemoryGlobPattern, resolveMemoryUrlToPath, decodeVaultPath, validateQueryPath) with '..' in the path.","commonSituations":"Building a URL by naive string concatenation of user input containing '..'; double-dot segments introduced by URL percent-decoding (%2e%2e); joining a sibling skill's path; glob patterns like '../**/*.md' passed through splitMemoryGlobPattern.","solutions":["Remove '..' segments — reference files only within the target skill's directory","Resolve the desired file to a path relative to skill.baseDir with path.relative() and verify it contains no '..'","For sibling or external files, resolve the other skill directly (skill://other-skill/...) or use file://","Decode and sanitize user input before constructing the URL"],"exampleFix":"// before\nresolve(`skill://my-skill/../shared/util.md`)\n// after\nresolve('skill://shared-skill/util.md') // resolve the owning skill directly","handlingStrategy":"validation","validationCode":"function assertNoTraversal(p: string): string {\n  const norm = path.normalize(decodeURIComponent(p));\n  if (norm.split(/[\\\\/]/).includes('..') || norm.includes('..')) {\n    throw new Error(`Traversal rejected: ${p}`);\n  }\n  return norm;\n}\n// run before building the skill:// URL from user input","typeGuard":"function isTraversalFree(p: string): boolean {\n  return !path.normalize(p).split(/[\\\\/]/).includes('..');\n}","tryCatchPattern":"try {\n  return await handler.resolve(url, ctx);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Path traversal')) {\n    logger.warn('Rejected traversal attempt', { url });\n  }\n  throw err;\n}","preventionTips":["Sanitize user-supplied paths before URL construction, after percent-decoding","Use path.relative(baseDir, resolvedTarget) and reject results starting with '..'","Treat any '..' in a URL path as malicious input, never a navigation feature"],"tags":["security","path-traversal","url"],"backgroundTag":"path-traversal-blocked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}