{"record":{"id":"7067ce79e9162b50","repo":"siyuan-note/siyuan","slug":"invalid-custom-emoji-name","errorCode":null,"errorMessage":"invalid custom emoji name","messagePattern":"invalid custom emoji name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/system.go","lineNumber":402,"sourceCode":"\treturn nil, \"\", fmt.Errorf(\"unsupported custom emoji image format\")\n}\n\nfunc normalizeCustomEmojiPath(name, ext string) (string, error) {\n\tname = strings.TrimSpace(strings.ReplaceAll(name, \"\\\\\", \"/\"))\n\tparts := strings.Split(name, \"/\")\n\tif len(parts) == 0 {\n\t\treturn \"\", fmt.Errorf(\"custom emoji name must not be empty\")\n\t}\n\n\tlastIndex := len(parts) - 1\n\tswitch strings.ToLower(filepath.Ext(parts[lastIndex])) {\n\tcase \".png\", \".jpg\", \".jpeg\", \".gif\", \".webp\", \".svg\":\n\t\tparts[lastIndex] = strings.TrimSuffix(parts[lastIndex], filepath.Ext(parts[lastIndex]))\n\t}\n\tfor i, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" || part == \".\" || part == \"..\" {\n\t\t\treturn \"\", fmt.Errorf(\"invalid custom emoji name\")\n\t\t}\n\t\tpart = util.FilterUploadFileName(part)\n\t\tif part == \"\" || part == \".\" || part == \"..\" {\n\t\t\treturn \"\", fmt.Errorf(\"invalid custom emoji name\")\n\t\t}\n\t\tparts[i] = part\n\t}\n\tparts[lastIndex] += ext\n\treturn strings.Join(parts, \"/\"), nil\n}\n\nfunc checkUpdate(c *gin.Context) {\n\tret := gulu.Ret.NewResult()\n\tdefer c.JSON(http.StatusOK, ret)\n\n\targ, ok := util.JsonArg(c, ret)\n\tif !ok {\n\t\treturn","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/api/system.go#L384-L420","documentation":"First throw of 'invalid custom emoji name' in normalizeCustomEmojiPath's loop. After TrimSpace, a path segment is empty, '.', or '..'. This catches empty segments (leading/trailing/double slash), current-directory, and parent-directory references — i.e. path traversal or malformed names supplied via the name form field.","triggerScenarios":"POST /api/system/addCustomEmoji with name like \"\", \"/icon\", \"a//b\", \"../icon\", \"./x\", or \".\" — any segment that is empty or a dot/dotdot after trimming. A trailing slash or a path-traversal attempt lands here.","commonSituations":"Client sends name with a trailing slash; user types a relative path hoping to write outside the emojis dir; front-end bug submits an empty name when the field is left blank.","solutions":["Provide a simple non-empty basename without slashes, e.g. \"myemoji\" or \"group/icon\".","Strip leading/trailing slashes and collapse repeated slashes before submitting.","Never send '.' or '..' segments; they are rejected to prevent escaping the emojis directory."],"exampleFix":"// before\nc.PostForm(\"name\") == \"../etc/passwd\"   // -> invalid custom emoji name\n\n// after: submit a flat, sanitized name\nname := \"myemoji\"   // or \"packs/happy\" for a subfolder\n// client side:\n//   name = name.trim().replace(/\\/+/g, '/').replace(/^\\/+|\\/+$/g, '')","handlingStrategy":"validation","validationCode":"// Reject path-traversal / empty segments before submitting the name\nname = strings.TrimSpace(strings.ReplaceAll(name, \"\\\\\", \"/\"))\nfor _, part := range strings.Split(name, \"/\") {\n    part = strings.TrimSpace(part)\n    if part == \"\" || part == \".\" || part == \"..\" {\n        return errors.New(\"name contains an empty or traversal segment\")\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Send a flat basename or a clean relative path with no leading/trailing slashes.","Never include '.' or '..' segments.","Collapse repeated slashes client-side before submit."],"tags":["validation","emoji","path-traversal","filename","api"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}