{"record":{"id":"5e257eff0e2e4dce","repo":"siyuan-note/siyuan","slug":"invalid-skill-name-s","errorCode":null,"errorMessage":"invalid skill name: %s","messagePattern":"invalid skill name: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":108,"sourceCode":"\t\tb, err := filelock.ReadFile(skillMdPath)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfm, body := parseSkillFrontmatter(string(b))\n\t\tskillName := fm[\"name\"]\n\t\tif skillName == \"\" {\n\t\t\tskillName = e.Name()\n\t\t}\n\t\tif strings.EqualFold(skillName, name) || strings.EqualFold(e.Name(), name) {\n\t\t\treturn body\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc validateSkillName(name string) error {\n\tif name == \"\" || name == \".\" || name == \"..\" {\n\t\treturn fmt.Errorf(\"invalid skill name: %s\", name)\n\t}\n\tif strings.ContainsAny(name, `/\\`) {\n\t\treturn fmt.Errorf(\"invalid skill name: %s\", name)\n\t}\n\tdir := SkillsDir()\n\tabs := filepath.Join(dir, name)\n\tif !gulu.File.IsSubPath(dir, abs) {\n\t\treturn fmt.Errorf(\"invalid skill name: %s\", name)\n\t}\n\treturn nil\n}\n\nfunc ReadSkill(name string) (string, error) {\n\tif err := validateSkillName(name); err != nil {\n\t\treturn \"\", err\n\t}\n\tskillMdPath := filepath.Join(SkillsDir(), name, \"SKILL.md\")\n\tb, err := filelock.ReadFile(skillMdPath)","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/skill.go#L90-L126","documentation":"Thrown by validateSkillName (kernel/util/skill.go:108) when the supplied skill name is the empty string, \".\", or \"..\". It is the first of three guards in validateSkillName and blocks trivially empty or self/parent-referential names before any filesystem operation. ReadSkill/SaveSkill/RemoveSkill/RenameSkill all run it first.","triggerScenarios":"Calling ReadSkill(\"\"), SaveSkill(\".\", ...), or RemoveSkill(\"..\") — typically a caller bug that passed an unset/zero-value name, or a UI form that submitted before a name was entered.","commonSituations":"Frontend sends a skill API request without filling the name field; a script iterates a list and forwards an empty entry; a rename operation passed an empty newName.","solutions":["Validate the name on the caller side before invoking the skill API (non-empty, not \".\" or \"..\").","Disable the submit button until a non-empty name is entered in the UI.","Audit the request payload to confirm the name field is being serialized."],"exampleFix":"// before\nerr := util.SaveSkill(req.Name, req.Content)\n\n// after\nname := strings.TrimSpace(req.Name)\nif name == \"\" || name == \".\" || name == \"..\" {\n    return errors.New(\"skill name must be a non-empty, non-traversal name\")\n}\nerr := util.SaveSkill(name, req.Content)","handlingStrategy":"validation","validationCode":"name := strings.TrimSpace(req.Name)\nif name == \"\" || name == \".\" || name == \"..\" {\n    return errors.New(\"skill name must be a non-empty value other than '.' or '..'\")\n}","typeGuard":null,"tryCatchPattern":"if err := util.ReadSkill(name); err != nil {\n    if strings.HasPrefix(err.Error(), \"invalid skill name\") {\n        // input-validation failure; surface to user, do not retry unchanged\n        return userFacingValidationError{err}\n    }\n    return err\n}","preventionTips":["Always TrimSpace and reject empty/./.. on the caller side before the skill API.","Disable submit in the UI until a non-trivial name is entered.","Add a unit test that the API rejects zero-value names."],"tags":["skill","input-validation","path-traversal","go"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}