{"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":410,"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\nvar checkUpdate = contractHandler(apicontract.SystemCheckUpdate, func(c *gin.Context, request apicontract.SystemCheckUpdateRequest) (ret apicontract.Response[apicontract.Null]) {\n\tret = apicontract.Success(apicontract.Null{})\n\n\tshowMsg := request.ShowMsg\n\tmodel.CheckUpdate(showMsg)\n\treturn\n})","sourceCodeStart":392,"sourceCodeEnd":428,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/api/system.go#L392-L428","documentation":"Each path component of the emoji name is trimmed and checked: empty strings, '.', and '..' are rejected as 'invalid custom emoji name' before any filtering, preventing path traversal and malformed names.","triggerScenarios":"The emoji name contains a path component that is empty, '.', or '..' after trimming — e.g. name '../etc/passwd.png', 'a//b.png', or ' ./x.png'.","commonSituations":"Untrusted user input used directly as a filename; path traversal attempts; copy-paste errors leaving stray slashes or dots in the name.","solutions":["Remove '.', '..', and empty segments from the name","Use a single flat filename like 'emoji.png' instead of a relative path with dot segments","Sanitize user input before submitting to the API","Never build emoji names from filesystem paths of untrusted origin"],"exampleFix":"// before\n{\"name\": \"../../emoji.png\"}\n// after\n{\"name\": \"emoji.png\"}","handlingStrategy":"validation","validationCode":"const parts = name.replace(/\\\\/g, \"/\").split(\"/\");\nif (parts.some(p => [\"\", \".\", \"..\"].includes(p.trim())))\n  throw new Error(\"emoji name contains invalid path segments\");","typeGuard":null,"tryCatchPattern":"try {\n  await registerEmoji({name});\n} catch (e) {\n  if (String(e).includes(\"invalid custom emoji name\")) {\n    notifyUser(\"Remove '.', '..', and empty segments from the name\");\n  }\n}","preventionTips":["Use flat filenames without path separators for emoji names","Never build names from untrusted filesystem paths","Strip dot-segments before submission"],"tags":["security","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}