{"record":{"id":"7ac31172a33e3132","repo":"siyuan-note/siyuan","slug":"generated-image-redirect-is-not-allowed","errorCode":null,"errorMessage":"generated image redirect is not allowed","messagePattern":"generated image redirect is not allowed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/openai.go","lineNumber":969,"sourceCode":"\tdata, err := io.ReadAll(io.LimitReader(resp.Body, maxGeneratedImageBytes+1))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(data) > maxGeneratedImageBytes {\n\t\treturn nil, errors.New(\"generated image exceeds size limit\")\n\t}\n\treturn data, nil\n}\n\nfunc generatedImageHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tProxy:       httpclient.ProxyFromEnvironment,\n\t\t\tDialContext: generatedImageDialer().DialContext,\n\t\t},\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\tif len(via) >= 3 || req.URL.Scheme != \"https\" {\n\t\t\t\treturn errors.New(\"generated image redirect is not allowed\")\n\t\t\t}\n\t\t\treturn CheckHostSSRF(req.URL.Hostname())\n\t\t},\n\t}\n}\n\nfunc generatedImageDialer() *net.Dialer {\n\treturn &net.Dialer{\n\t\tTimeout: 30 * time.Second,\n\t\tControl: func(_, address string, _ syscall.RawConn) error {\n\t\t\thost, _, err := net.SplitHostPort(address)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tip, parseErr := netip.ParseAddr(host)\n\t\t\tif parseErr != nil || isUnsafeGeneratedImageIP(ip.Unmap()) {\n\t\t\t\treturn errors.New(\"generated image URL resolved to a private or invalid IP\")\n\t\t\t}","sourceCodeStart":951,"sourceCodeEnd":987,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/openai.go#L951-L987","documentation":"The image download HTTP client installs a CheckRedirect hook as part of SSRF protection: at most 2 redirects are followed, every redirected URL must remain https, and each hop's host must pass CheckHostSSRF. Violating any of these aborts with this error, so a provider cannot bounce the request to http:// or to an internal address.","triggerScenarios":"The image URL responds with 3xx Location that is (a) the 3rd+ redirect in the chain, (b) an http:// or non-https scheme, or the next hop's hostname fails CheckHostSSRF.","commonSituations":"CDN redirecting https → http; redirect chains longer than 2 hops (auth gateway → CDN); redirect to an internal/private hostname (classic SSRF); expired URL redirecting to a login page over http.","solutions":["Get the provider to return a direct, non-redirecting https image URL","If a redirect chain is legitimate, increase the hop limit in generatedImageHTTPClient's CheckRedirect (only for trusted hosts)","Fix the server so redirects preserve https scheme","Ensure the final host is a public address that passes CheckHostSSRF"],"exampleFix":"// before\nLocation: \"http://cdn.example.com/img.png\" // https->http redirect blocked\n// after\nLocation: \"https://cdn.example.com/img.png\" // allowed (scheme stays https)","handlingStrategy":"validation","validationCode":"func nextHopOK(u *url.URL) error {\n    if u.Scheme != \"https\" || u.Host == \"\" { return errors.New(\"bad redirect target\") }\n    return CheckHostSSRF(u.Hostname())\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, errRedirectBlocked) || strings.Contains(err.Error(), \"redirect is not allowed\") {\n    return fmt.Errorf(\"image host redirected unsafely; refusing download\")\n}","preventionTips":["Always set CheckRedirect on clients fetching provider-controlled URLs","Re-run SSRF host checks on every redirect hop","Keep the redirect limit small for untrusted hosts"],"tags":["security","ssrf","redirect","https"],"backgroundTag":"unsafe-redirect-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"}