{"record":{"id":"d6b6a82fb945d08b","repo":"siyuan-note/siyuan","slug":"invalid-skill-resource-path-s","errorCode":null,"errorMessage":"invalid skill resource path: %s","messagePattern":"invalid skill resource path: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":289,"sourceCode":"\t}\n\tresult.Content = content\n\tresult.ResourcePath = resource\n\treturn result, nil\n}\n\nfunc splitSkillLocator(locator string) (name, resource string) {\n\tlocator = strings.TrimSpace(strings.ReplaceAll(locator, `\\`, \"/\"))\n\tname, resource, _ = strings.Cut(locator, \"/\")\n\treturn\n}\n\nfunc normalizeSkillResourcePath(resource string) (string, error) {\n\tresource = strings.ReplaceAll(resource, `\\`, \"/\")\n\tcleaned := path.Clean(resource)\n\tnative := filepath.FromSlash(cleaned)\n\tif cleaned == \".\" || path.IsAbs(cleaned) || cleaned == \"..\" || strings.HasPrefix(cleaned, \"../\") ||\n\t\tfilepath.IsAbs(native) || filepath.VolumeName(native) != \"\" || hasWindowsDrivePrefix(cleaned) {\n\t\treturn \"\", fmt.Errorf(\"invalid skill resource path: %s\", resource)\n\t}\n\treturn cleaned, nil\n}\n\nfunc hasWindowsDrivePrefix(resource string) bool {\n\tif len(resource) < 2 || resource[1] != ':' {\n\t\treturn false\n\t}\n\tdrive := resource[0]\n\treturn 'a' <= drive && drive <= 'z' || 'A' <= drive && drive <= 'Z'\n}\n\nfunc listSkillResources(skillDir string) (resources []string, truncated bool) {\n\trealRoot, err := filepath.EvalSymlinks(skillDir)\n\tif err != nil {\n\t\treturn nil, false\n\t}\n","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/util/skill.go#L271-L307","documentation":"normalizeSkillResourcePath sanitizes the resource portion of a skill locator before it is joined to the skill directory. It rejects absolute paths, parent traversals ('..'), Windows drive/volume prefixes, and backslash-based escape attempts, returning this error for anything unsafe.","triggerScenarios":"Calling LoadSkill with a locator whose resource part is absolute ('/etc/passwd', 'C:\\x'), contains '..' segments, has a Windows drive prefix, or resolves to '.'/'..' after path.Clean.","commonSituations":"LLM-generated resource paths containing traversal sequences; copying Windows-style paths with backslashes or drive letters into the locator; attempting to read files outside the skill directory on purpose.","solutions":["Use a relative path within the skill directory (e.g. 'scripts/helper.md')","Strip leading '/' and any '..' segments from the resource path before calling","Convert backslashes to forward slashes and remove drive letters","Reference the file via the skill's declared bundled resources list"],"exampleFix":"// before\nLoadSkill(\"my-skill/../../../etc/passwd\", enabled)\n// after\nLoadSkill(\"my-skill/references/usage.md\", enabled)","handlingStrategy":"validation","validationCode":"function isSafeResourcePath(p) {\n  const c = p.replace(/\\\\/g, \"/\");\n  return !c.startsWith(\"/\") && !/^[A-Za-z]:/.test(c) &&\n    c !== \".\" && !c.split(\"/\").includes(\"..\");\n}\nif (!isSafeResourcePath(resource)) sanitizeBeforeCall(resource);","typeGuard":null,"tryCatchPattern":"try {\n  return await loadSkillResource(locator);\n} catch (e) {\n  if (String(e).includes(\"invalid skill resource path\")) {\n    return resolveInsideSkillOnly(locator); // rewrite the path relative to the skill\n  }\n  throw e;\n}","preventionTips":["Always build resource locators from skill-relative relative paths","Strip drive letters, backslashes, and '..' segments from any user/LLM-supplied path","Never construct locators from unvalidated external input"],"tags":["skills","path-traversal","validation","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}