{"record":{"id":"beb6b28471be90ab","repo":"Tencent/WeKnora","slug":"url-rejected-for-security-reasons-v","errorCode":null,"errorMessage":"URL rejected for security reasons: %v","messagePattern":"URL rejected for security reasons: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/im/wecom/webhook_adapter.go","lineNumber":572,"sourceCode":"\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"get access token: %w\", err)\n\t}\n\n\tapiURL := fmt.Sprintf(\"%s/cgi-bin/media/get?access_token=%s&media_id=%s\",\n\t\ta.apiBaseURL, accessToken, msg.FileKey)\n\treturn downloadFromURL(ctx, apiURL, fileName, a.extraAllowedHost)\n}\n\n// downloadFromURL performs a GET request and returns the response body.\n// It tries to resolve the real filename from HTTP response headers:\n//  1. Content-Disposition: attachment; filename=\"xxx.pdf\"\n//  2. Content-Type → extension mapping (fallback for platforms like WeCom that\n//     don't provide the original filename in the callback JSON)\nfunc downloadFromURL(ctx context.Context, rawURL, fileName string, extraAllowedHost string) (io.ReadCloser, string, error) {\n\t// SSRF protection: reject internal/private URLs unless on the WeCom API allowlist.\n\tif !isAllowedIMAPIHost(rawURL, extraAllowedHost) {\n\t\tif err := secutils.ValidateURLForSSRF(rawURL); err != nil {\n\t\t\treturn nil, \"\", fmt.Errorf(\"URL rejected for security reasons: %v\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"download: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tresp.Body.Close()\n\t\treturn nil, \"\", fmt.Errorf(\"download failed: status=%d\", resp.StatusCode)\n\t}\n","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/im/wecom/webhook_adapter.go#L554-L590","documentation":"downloadFromURL enforces SSRF protection: URLs not on the WeCom API host allowlist are passed to secutils.ValidateURLForSSRF, which rejects private/internal addresses. If that validation fails, the URL is rejected with 'URL rejected for security reasons: <reason>'. This stops attacker-controlled callback fields (e.g. a forged PicUrl) from making the server fetch internal resources.","triggerScenarios":"A message whose PicUrl/FileKey points to a private IP (10.x, 192.168.x, 169.254.169.254, localhost), a non-HTTP scheme, or any non-WeCom host not in extraAllowedHost — typically forged or replayed callbacks, or self-hosted test URLs.","commonSituations":"Local development pointing PicUrl at a local file server; test fixtures using example.com or internal hosts; genuinely malicious callbacks probing SSRF; WeCom changing media CDN hostnames so new hosts fail the allowlist check.","solutions":["Only download URLs that come from verified WeCom callbacks (msg_signature verified) and use official WeCom media hosts","For tests/dev, pass the local host via the extraAllowedHost parameter of downloadFromURL / adapter configuration instead of bypassing validation","If a new legitimate WeCom host is rejected, add it to the allowlist configuration — never disable SSRF validation","Inspect the wrapped %v reason (e.g. private IP, bad scheme) to understand which rule fired"],"exampleFix":"// before: test fails against local server\nrc, _, err := downloadFromURL(ctx, \"http://localhost:8080/file\", name, \"\")\n// after\nrc, _, err := downloadFromURL(ctx, \"http://localhost:8080/file\", name, \"localhost:8080\")","handlingStrategy":"validation","validationCode":"u, err := url.Parse(msg.FileKey)\nif err != nil || (u.Scheme != \"https\" && u.Scheme != \"http\") || isPrivateHost(u.Hostname()) {\n    return errors.New(\"untrusted file URL\")\n}","typeGuard":"func isPublicHTTPURL(raw string) bool {\n    u, err := url.Parse(raw)\n    return err == nil && (u.Scheme == \"http\" || u.Scheme == \"https\") &&\n        net.ParseIP(u.Hostname()) != nil && !isPrivateIP(net.ParseIP(u.Hostname()))\n}","tryCatchPattern":"rc, name, err := adapter.DownloadFile(ctx, msg)\nif err != nil && strings.Contains(err.Error(), \"URL rejected for security reasons\") {\n    logger.Warnf(\"blocked suspicious media URL: %v\", err)\n    return ErrUntrustedURL\n}","preventionTips":["Never disable SSRF validation in production","Configure extraAllowedHost explicitly for tests instead of loosening checks","Verify msg_signature so attacker-chosen PicUrl values never reach the downloader","Keep the WeCom media host allowlist updated with security review"],"tags":["ssrf","security","download","url-validation","wecom"],"backgroundTag":"ssrf-blocked","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}