app: reject browser origins on Claude Desktop gateway (#17989)

This commit is contained in:
Parth Sareen
2026-08-25 09:48:44 -07:00
committed by GitHub
parent f6c59d8703
commit 6e19e916c7
2 changed files with 63 additions and 0 deletions
+7
View File
@@ -302,6 +302,13 @@ func (p *ClaudeDesktop) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// Claude Desktop uses a native HTTP client, not a browser. Reject every
// request carrying an Origin so the upstream OLLAMA_ORIGINS policy cannot
// enable CORS on this loopback-only gateway.
if r.Header.Get("Origin") != "" {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if r.URL.Path == healthPath {
if r.Method != http.MethodGet {
+56
View File
@@ -1138,6 +1138,62 @@ func TestGatewayRejectsNonLoopbackHost(t *testing.T) {
}
}
func TestGatewayRejectsRequestsWithOrigin(t *testing.T) {
var upstreamCalls atomic.Int32
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
upstreamCalls.Add(1)
w.Header().Set("Access-Control-Allow-Origin", "*")
}))
defer upstream.Close()
p := startTestGateway(t, upstream.URL)
messageBody, err := json.Marshal(map[string]any{
"model": p.Models()[0].GatewayID(),
"messages": []any{},
})
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
name string
method string
path string
origin string
body string
}{
{"messages from public site", http.MethodPost, "/v1/messages", "https://attacker.example", string(messageBody)},
{"messages from localhost site", http.MethodPost, "/v1/messages", "http://localhost:3000", string(messageBody)},
{"messages with opaque origin", http.MethodPost, "/v1/messages", "null", string(messageBody)},
{"models", http.MethodGet, "/v1/models", "https://attacker.example", ""},
{"token count", http.MethodPost, "/v1/messages/count_tokens", "https://attacker.example", string(messageBody)},
{"health", http.MethodGet, healthPath, "https://attacker.example", ""},
{"preflight", http.MethodOptions, "/v1/messages", "https://attacker.example", ""},
} {
t.Run(test.name, func(t *testing.T) {
req, err := http.NewRequest(test.method, "http://"+p.Addr()+test.path, strings.NewReader(test.body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Origin", test.origin)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("status = %d, want 403", resp.StatusCode)
}
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "" {
t.Fatalf("Access-Control-Allow-Origin = %q, want empty", got)
}
})
}
if got := upstreamCalls.Load(); got != 0 {
t.Fatalf("upstream calls = %d, want 0", got)
}
}
func TestGatewayRejectsHostWithWrongPort(t *testing.T) {
var upstreamCalls atomic.Int32
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {