{"record":{"id":"474a84308589e4cf","repo":"siyuan-note/siyuan","slug":"invalid-custom-emoji-url","errorCode":null,"errorMessage":"invalid custom emoji URL","messagePattern":"invalid custom emoji URL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/system.go","lineNumber":331,"sourceCode":"\t\tfile, err := fileHeader.Open()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer file.Close()\n\t\treturn io.ReadAll(io.LimitReader(file, maxCustomEmojiSize+1))\n\t}\n\n\trawURL := strings.TrimSpace(c.PostForm(\"url\"))\n\tif rawURL == \"\" {\n\t\treturn nil, fmt.Errorf(\"field [file] or [url] must not be empty\")\n\t}\n\treturn downloadCustomEmojiData(rawURL)\n}\n\nfunc downloadCustomEmojiData(rawURL string) ([]byte, error) {\n\tparsedURL, err := url.Parse(rawURL)\n\tif err != nil || (parsedURL.Scheme != \"http\" && parsedURL.Scheme != \"https\") || parsedURL.Host == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid custom emoji URL\")\n\t}\n\n\tresponse, err := util.NewCustomReqClient().R().Get(parsedURL.String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"download custom emoji failed: %w\", err)\n\t}\n\tdefer response.Body.Close()\n\tif response.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"download custom emoji failed with status %d\", response.StatusCode)\n\t}\n\tif response.ContentLength > maxCustomEmojiSize {\n\t\treturn nil, fmt.Errorf(\"custom emoji file is too large\")\n\t}\n\n\tdata, err := io.ReadAll(io.LimitReader(response.Body, maxCustomEmojiSize+1))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read custom emoji response failed: %w\", err)\n\t}","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/api/system.go#L313-L349","documentation":"Returned by downloadCustomEmojiData (system.go:331) when the `url` POST field is not a valid HTTP/HTTPS URL with a host. url.Parse failed, the scheme was something other than http/https (e.g. file://, javascript:, ftp:), or the host was empty. This guard runs before any network request to block non-HTTP schemes and malformed URLs.","triggerScenarios":"Submitting url='ftp://host/x', url='file:///etc/passwd', url='javascript:alert(1)', url='/local/path' (relative), url='example.com/x' (no scheme), or an unparseable string. The check at system.go:330 fires and returns the error at line 331.","commonSituations":"User pasted a local file path or relative URL. A non-http scheme was used (intentionally or by mistake). Input contained control characters or was not trimmed. Security-relevant: this guard blocks SSRF-via-scheme and local-file exfiltration.","solutions":["Supply an absolute http:// or https:// URL with a non-empty host.","Trim/validate the URL client-side and reject non-http(s) schemes before submitting.","If the user selected a local file, use the 'file' upload mode instead of the URL mode."],"exampleFix":"// before\nfd.append('url', '/local/emoji.png')\n// after\nfd.append('url', 'https://cdn.example.com/emoji.png')","handlingStrategy":"validation","validationCode":"function validEmojiUrl(u) {\n  try { const p = new URL(u); return (p.protocol === 'http:' || p.protocol === 'https:') && !!p.host; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Reject non-http(s) schemes and relative URLs client-side.","Trim and validate the URL before submit.","For local images, use the 'file' upload mode rather than a file:// URL."],"tags":["emoji","network","security","validation","kernel"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}