{"record":{"id":"ad5467e43b4778ac","repo":"can1357/oh-my-pi","slug":"path-traversal-is-not-allowed","errorCode":null,"errorMessage":"Path traversal is not allowed","messagePattern":"Path traversal is not allowed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/skill-protocol.ts","lineNumber":78,"sourceCode":"\t\tif (!skill) {\n\t\t\tconst available = skills.map(s => s.name);\n\t\t\tconst availableStr = available.length > 0 ? available.join(\", \") : \"none\";\n\t\t\tthrow new Error(`Unknown skill: ${skillName}\\nAvailable: ${availableStr}`);\n\t\t}\n\n\t\tlet targetPath: string;\n\t\tconst urlPath = url.pathname;\n\t\tconst hasRelativePath = urlPath && urlPath !== \"/\" && urlPath !== \"\";\n\n\t\tif (hasRelativePath) {\n\t\t\tconst relativePath = decodeURIComponent(urlPath.slice(1));\n\t\t\tvalidateRelativePath(relativePath);\n\t\t\ttargetPath = path.join(skill.baseDir, relativePath);\n\n\t\t\tconst resolvedPath = path.resolve(targetPath);\n\t\t\tconst resolvedBaseDir = path.resolve(skill.baseDir);\n\t\t\tif (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {\n\t\t\t\tthrow new Error(\"Path traversal is not allowed\");\n\t\t\t}\n\t\t\t// Agent Plugin skills (§4.1): the resource must canonically resolve\n\t\t\t// within the plugin root; a dangling or unresolvable path fails closed.\n\t\t\t// Symlinks may target other files inside the same package.\n\t\t\tif (skill.containRoot) {\n\t\t\t\tconst contained = await resolveContainedPath(skill.containRoot, resolvedPath);\n\t\t\t\tif (contained.status === \"outside\") {\n\t\t\t\t\tthrow new Error(`skill:// path resolves outside the plugin root: ${url.href}`);\n\t\t\t\t}\n\t\t\t\tif (contained.status === \"missing\") {\n\t\t\t\t\tthrow new Error(`File not found: ${resolvedPath}`);\n\t\t\t\t}\n\t\t\t\ttargetPath = contained.realPath;\n\t\t\t}\n\t\t} else {\n\t\t\ttargetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;\n\t\t}\n","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/skill-protocol.ts#L60-L96","documentation":"This is a second, post-join defense in resolve(): even after validateRelativePath() passes, the joined path is resolved with path.resolve() and checked to still start with the skill's resolved baseDir. It catches edge cases the segment check misses (e.g. symlinks within baseDir pointing out, unusual normalization). If the resolved absolute path escapes skill.baseDir, resolve() refuses with this generic traversal error.","triggerScenarios":"Resolving skill://<name>/<path> where path.join(skill.baseDir, relativePath), after path.resolve(), lands outside path.resolve(skill.baseDir) — e.g. via a symlinked directory inside the skill whose target is external (when the skill has no containRoot), or platform-specific normalization quirks.","commonSituations":"A symlink inside the skill directory pointing to an external file; baseDir itself being a symlink whose resolution interacts with the relative path; paths crafted with separators that evade the '..' segment check but still escape after resolution (e.g. on Windows with mixed separators).","solutions":["Reference real files inside the skill directory, not symlinks pointing outside it","Inspect where skill.baseDir and the target actually resolve (path.resolve, fs.realpath) to find the escape","If you legitimately need external content, expose it as its own skill or resource rather than symlinking into a skill","Move the external file into the skill's directory"],"exampleFix":"// before\n// skills/my-skill/data -> symlink to /etc  ;  resolve('skill://my-skill/data/passwd')\n// after\n// copy the needed files physically into the skill directory and resolve them there","handlingStrategy":"validation","validationCode":"const resolved = path.resolve(path.join(skill.baseDir, rel));\nconst base = path.resolve(skill.baseDir);\nif (!resolved.startsWith(base + path.sep) && resolved !== base) {\n  throw new Error(`skill path escapes baseDir: ${resolved}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await handler.resolve(url, ctx);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Path traversal is not allowed') {\n    // audit the skill directory for symlinks pointing outside\n  }\n  throw err;\n}","preventionTips":["Keep skill directories free of symlinks to external targets","Pre-verify with path.resolve + startsWith before constructing URLs","Prefer physical copies over symlinks inside skill packages"],"tags":["security","path-traversal","symlink"],"backgroundTag":"path-traversal-blocked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}