{"record":{"id":"97d68aedfe0245b2","repo":"flipped-aurora/gin-vue-admin","slug":"error-97d68a","errorCode":null,"errorMessage":"技能名称不合法","messagePattern":"技能名称不合法","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/service/system/sys_skills.go","lineNumber":92,"sourceCode":"\t}\n\tentries, err := os.ReadDir(skillsDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar skills []string\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tskills = append(skills, entry.Name())\n\t\t}\n\t}\n\tsort.Strings(skills)\n\treturn skills, nil\n}\n\nfunc (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) {\n\tvar detail system.SkillDetail\n\tif !isSafeName(skill) {\n\t\treturn detail, errors.New(\"技能名称不合法\")\n\t}\n\tdetail.Tool = tool\n\tdetail.Skill = skill\n\n\tskillDir, err := s.skillDir(tool, skill)\n\tif err != nil {\n\t\treturn detail, err\n\t}\n\n\tskillFilePath := filepath.Join(skillDir, skillFileName)\n\tcontent, err := os.ReadFile(skillFilePath)\n\tif err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn detail, err\n\t\t}\n\t\tdetail.Meta = system.SkillMeta{Name: skill}\n\t\tdetail.Markdown = defaultSkillMarkdown\n\t} else {","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/service/system/sys_skills.go#L74-L110","documentation":"SkillsService.Detail validates the skill name with isSafeName before resolving its directory on disk. A name failing this check (empty, path separators, traversal like ../, or other unsafe characters) is rejected with this error to prevent path traversal outside the skills root.","triggerScenarios":"Calling Detail(tool, skill) with a skill value that is empty or contains '/', '\\\\', '..', or other characters rejected by isSafeName.","commonSituations":"Client sends a skill name copied from a file path (e.g. 'skills/my-skill/SKILL.md'); crafted URLs attempting directory traversal; frontend sends empty skill param after a bad selection.","solutions":["Pass only the bare skill directory name, without path separators or '..' segments.","Trim and re-validate the skill value on the client before sending the request.","If the name legitimately needs normalization, sanitize it server-side before calling Detail."],"exampleFix":"// before\ndetail, err := skillsService.Detail(ctx, \"claude\", \"../../etc\")\n// after\ndetail, err := skillsService.Detail(ctx, \"claude\", \"my-skill\")","handlingStrategy":"validation","validationCode":"const safeNameRe = /^[A-Za-z0-9][A-Za-z0-9._-]*$/\nif !safeNameRe.test(skill) throw new Error(\"invalid skill name: \" + skill)\nawait api.getSkillDetail(tool, skill)","typeGuard":"func isSafeSkillName(s string) bool {\n\tif s == \"\" { return false }\n\tif strings.ContainsAny(s, \"/\\\\\") { return false }\n\treturn !strings.Contains(s, \"..\")\n}","tryCatchPattern":"try {\n  await api.getSkillDetail(tool, skill)\n} catch (e) {\n  if (e.response?.data?.msg === \"技能名称不合法\") {\n    showInputError(\"技能名称只能包含字母、数字、点、下划线和中划线\")\n  }\n}","preventionTips":["Always send bare skill names from server-provided list data, never user-typed paths.","Normalize/trim input before sending.","Reject names with separators or '..' client-side for immediate feedback."],"tags":["validation","path-traversal","input-sanitization"],"backgroundTag":"invalid-name-validation","analyzedSha":"3136500ef380842b0eb6c4daa87c3f8a47fcf9e0","analyzedAt":"2026-08-31T13:50:02.721Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}