mirror of
https://github.com/ollama/ollama.git
synced 2026-01-29 07:12:03 +03:00
Added validation to ensure auth redirects stay on the same host as the original request. The fix is a single check in getAuthorizationToken comparing the realm URL's host against the request host. Added tests for the auth flow. Co-Authored-By: Gecko Security <188164982+geckosecurity@users.noreply.github.com> * gofmt --------- Co-authored-by: Gecko Security <188164982+geckosecurity@users.noreply.github.com>
114 lines
3.1 KiB
Go
114 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGetAuthorizationTokenRejectsCrossDomain(t *testing.T) {
|
|
tests := []struct {
|
|
realm string
|
|
originalHost string
|
|
wantMismatch bool
|
|
}{
|
|
{"https://example.com/token", "example.com", false},
|
|
{"https://example.com/token", "other.com", true},
|
|
{"https://example.com/token", "localhost:8000", true},
|
|
{"https://localhost:5000/token", "localhost:5000", false},
|
|
{"https://localhost:5000/token", "localhost:6000", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.originalHost, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
challenge := registryChallenge{Realm: tt.realm, Service: "test", Scope: "repo:x:pull"}
|
|
_, err := getAuthorizationToken(ctx, challenge, tt.originalHost)
|
|
|
|
isMismatch := err != nil && strings.Contains(err.Error(), "does not match")
|
|
if tt.wantMismatch && !isMismatch {
|
|
t.Errorf("expected domain mismatch error, got: %v", err)
|
|
}
|
|
if !tt.wantMismatch && isMismatch {
|
|
t.Errorf("unexpected domain mismatch error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRegistryChallenge(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantRealm, wantService, wantScope string
|
|
}{
|
|
{
|
|
`Bearer realm="https://auth.example.com/token",service="registry",scope="repo:foo:pull"`,
|
|
"https://auth.example.com/token", "registry", "repo:foo:pull",
|
|
},
|
|
{
|
|
`Bearer realm="https://r.ollama.ai/v2/token",service="ollama",scope="-"`,
|
|
"https://r.ollama.ai/v2/token", "ollama", "-",
|
|
},
|
|
{"", "", "", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := parseRegistryChallenge(tt.input)
|
|
if result.Realm != tt.wantRealm || result.Service != tt.wantService || result.Scope != tt.wantScope {
|
|
t.Errorf("parseRegistryChallenge(%q) = {%q, %q, %q}, want {%q, %q, %q}",
|
|
tt.input, result.Realm, result.Service, result.Scope,
|
|
tt.wantRealm, tt.wantService, tt.wantScope)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryChallengeURL(t *testing.T) {
|
|
challenge := registryChallenge{
|
|
Realm: "https://auth.example.com/token",
|
|
Service: "registry",
|
|
Scope: "repo:foo:pull repo:bar:push",
|
|
}
|
|
|
|
u, err := challenge.URL()
|
|
if err != nil {
|
|
t.Fatalf("URL() error: %v", err)
|
|
}
|
|
|
|
if u.Host != "auth.example.com" {
|
|
t.Errorf("host = %q, want %q", u.Host, "auth.example.com")
|
|
}
|
|
if u.Path != "/token" {
|
|
t.Errorf("path = %q, want %q", u.Path, "/token")
|
|
}
|
|
|
|
q := u.Query()
|
|
if q.Get("service") != "registry" {
|
|
t.Errorf("service = %q, want %q", q.Get("service"), "registry")
|
|
}
|
|
if scopes := q["scope"]; len(scopes) != 2 {
|
|
t.Errorf("scope count = %d, want 2", len(scopes))
|
|
}
|
|
if q.Get("ts") == "" {
|
|
t.Error("missing ts")
|
|
}
|
|
if q.Get("nonce") == "" {
|
|
t.Error("missing nonce")
|
|
}
|
|
|
|
// Nonces should differ between calls
|
|
u2, _ := challenge.URL()
|
|
if q.Get("nonce") == u2.Query().Get("nonce") {
|
|
t.Error("nonce should be unique per call")
|
|
}
|
|
}
|
|
|
|
func TestRegistryChallengeURLInvalid(t *testing.T) {
|
|
challenge := registryChallenge{Realm: "://invalid"}
|
|
if _, err := challenge.URL(); err == nil {
|
|
t.Error("expected error for invalid URL")
|
|
}
|
|
}
|