diff --git a/backend/internal/pkg/xai/oauth.go b/backend/internal/pkg/xai/oauth.go index d401bf6a41..a8a549c9dc 100644 --- a/backend/internal/pkg/xai/oauth.go +++ b/backend/internal/pkg/xai/oauth.go @@ -191,7 +191,7 @@ func RuntimeSanity() RuntimeSanityReport { UnsafeURLOverrides: AllowUnsafeURLOverrides(), UnsafeHighConcurrency: AllowUnsafeHighConcurrency(), PublicGatewayScope: "responses_only", - ProxyPolicy: "account_proxy_optional; upstream URL allowlists enforced unless unsafe overrides are enabled", + ProxyPolicy: "account_proxy_optional; OAuth URLs use trusted-host allowlists; API-key base URLs require public HTTPS unless unsafe overrides are enabled", } } @@ -252,6 +252,19 @@ func ValidateOAuthEndpointURL(raw string) (string, error) { } func ValidateBaseURL(raw string) (string, error) { + if AllowUnsafeURLOverrides() { + return urlvalidator.ValidateURLFormat(raw, true) + } + normalized, err := urlvalidator.ValidateHTTPSURL(raw, urlvalidator.ValidationOptions{ + AllowPrivate: false, + }) + if err != nil { + return "", err + } + return normalizeKnownBaseURLPath(normalized) +} + +func ValidateTrustedBaseURL(raw string) (string, error) { if AllowUnsafeURLOverrides() { return urlvalidator.ValidateURLFormat(raw, true) } diff --git a/backend/internal/pkg/xai/oauth_test.go b/backend/internal/pkg/xai/oauth_test.go index 732977780b..39200ffc39 100644 --- a/backend/internal/pkg/xai/oauth_test.go +++ b/backend/internal/pkg/xai/oauth_test.go @@ -145,13 +145,10 @@ func TestBuildGrokMediaURLs(t *testing.T) { require.Error(t, err) } -func TestValidateXAIURLsRejectArbitraryHostsByDefault(t *testing.T) { +func TestValidateXAIURLsRejectUntrustedOAuthAndUnsafeBaseURLsByDefault(t *testing.T) { _, err := ValidateOAuthEndpointURL("https://auth.example.test/oauth2/token") require.Error(t, err) - _, err = ValidateBaseURL("https://xai.test/v1") - require.Error(t, err) - _, err = ValidateBaseURL("http://127.0.0.1:8080/v1") require.Error(t, err) @@ -159,6 +156,15 @@ func TestValidateXAIURLsRejectArbitraryHostsByDefault(t *testing.T) { require.Error(t, err) } +func TestValidateBaseURLAllowsPublicThirdPartyGrokAPI(t *testing.T) { + baseURL, err := ValidateBaseURL("https://grok.example.test/v1/") + require.NoError(t, err) + require.Equal(t, "https://grok.example.test/v1", baseURL) + + _, err = ValidateTrustedBaseURL("https://grok.example.test/v1") + require.Error(t, err) +} + func TestValidateXAIURLsAllowUnsafeDevOverride(t *testing.T) { t.Setenv(EnvAllowUnsafeURLOverrides, "true") @@ -190,6 +196,7 @@ func TestRuntimeSanityReportsSafeDefaults(t *testing.T) { require.False(t, report.UnsafeHighConcurrency) require.Equal(t, "responses_only", report.PublicGatewayScope) require.Contains(t, report.ProxyPolicy, "account_proxy_optional") + require.Contains(t, report.ProxyPolicy, "API-key base URLs require public HTTPS") } func TestRuntimeSanityReportsInvalidOverridesWithoutSecrets(t *testing.T) { diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index 62cb5a2e66..9413043e23 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -1260,6 +1260,10 @@ func (a *Account) GetGrokBaseURL() string { if strings.TrimSpace(baseURL) == "" || isOfficialGrokAPIBaseURL(baseURL) { return xai.DefaultCLIBaseURL } + if _, err := xai.ValidateTrustedBaseURL(baseURL); err == nil { + return baseURL + } + return xai.DefaultCLIBaseURL } if baseURL != "" { return baseURL diff --git a/backend/internal/service/account_base_url_test.go b/backend/internal/service/account_base_url_test.go index 0ffaa21ae4..e86d6c8c1a 100644 --- a/backend/internal/service/account_base_url_test.go +++ b/backend/internal/service/account_base_url_test.go @@ -266,7 +266,7 @@ func TestGetGrokBaseURLUsesSubscriptionProxyForOAuth(t *testing.T) { expected: "https://api.x.ai:8443/v1", }, { - name: "oauth explicit custom base_url remains supported", + name: "oauth explicit custom base_url stays pinned to CLI proxy by default", account: Account{ Type: AccountTypeOAuth, Platform: PlatformGrok, @@ -274,7 +274,7 @@ func TestGetGrokBaseURLUsesSubscriptionProxyForOAuth(t *testing.T) { "base_url": "https://custom.example.com/v1", }, }, - expected: "https://custom.example.com/v1", + expected: xai.DefaultCLIBaseURL, }, { name: "API key without base_url uses official credit-backed API", @@ -293,3 +293,16 @@ func TestGetGrokBaseURLUsesSubscriptionProxyForOAuth(t *testing.T) { }) } } + +func TestGetGrokBaseURLAllowsExplicitOAuthOverrideWhenUnsafeOverridesEnabled(t *testing.T) { + t.Setenv(xai.EnvAllowUnsafeURLOverrides, "true") + account := Account{ + Type: AccountTypeOAuth, + Platform: PlatformGrok, + Credentials: map[string]any{ + "base_url": "https://custom.example.com/v1", + }, + } + + require.Equal(t, "https://custom.example.com/v1", account.GetGrokBaseURL()) +} diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index 0c451767fc..f0067e99a2 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -271,7 +271,22 @@ func TestBuildGrokResponsesRequestUsesAccountBaseURLAndBearerToken(t *testing.T) require.Equal(t, `{"model":"grok-4.3"}`, strings.TrimSpace(string(data))) } -func TestBuildGrokResponsesRequestRejectsUnsafeAccountBaseURL(t *testing.T) { +func TestBuildGrokResponsesRequestAllowsPublicAPIKeyBaseURLByDefault(t *testing.T) { + account := &Account{ + Platform: PlatformGrok, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "base_url": "https://grok.example.test/v1/", + }, + } + + req, err := buildGrokResponsesRequest(context.Background(), nil, account, []byte(`{"model":"grok-4.3"}`), "api-key", "") + require.NoError(t, err) + require.Equal(t, "https://grok.example.test/v1/responses", req.URL.String()) + require.Equal(t, "Bearer api-key", req.Header.Get("Authorization")) +} + +func TestBuildGrokResponsesRequestPinsOAuthCustomBaseURLByDefault(t *testing.T) { t.Parallel() account := &Account{ @@ -282,9 +297,9 @@ func TestBuildGrokResponsesRequestRejectsUnsafeAccountBaseURL(t *testing.T) { }, } - _, err := buildGrokResponsesRequest(context.Background(), nil, account, []byte(`{"model":"grok-4.3"}`), "access-token", "") - require.Error(t, err) - require.Contains(t, err.Error(), "invalid base url") + req, err := buildGrokResponsesRequest(context.Background(), nil, account, []byte(`{"model":"grok-4.3"}`), "access-token", "") + require.NoError(t, err) + require.Equal(t, xai.DefaultCLIBaseURL+"/responses", req.URL.String()) } func TestGrokMediaGenerationGateCoversImagesAndVideo(t *testing.T) {