From abcb9614bcc497562e58f6418f5ddfcf2289fd01 Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 2 Apr 2026 08:49:54 +0700 Subject: [PATCH] feat(providers): allow provider_type changes on update with SSRF re-validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove blanket rejection of provider_type on update. Instead, when provider_type changes, re-validate the existing api_base against the new type to prevent SSRF via ACP→non-ACP type switch. This enables provider type changes in onboarding/setup flows while maintaining security: ACP providers skip URL validation, so switching from ACP to another type now forces URL re-validation. --- internal/http/providers.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/http/providers.go b/internal/http/providers.go index f4996fd4..560ff506 100644 --- a/internal/http/providers.go +++ b/internal/http/providers.go @@ -376,13 +376,6 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R } } - // provider_type is immutable after creation — changing it would bypass - // security checks (e.g. ACP skips URL validation). Reject if client tries. - if _, ok := updates["provider_type"]; ok { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, "provider_type cannot be changed after creation")}) - return - } - // Strip masked API key — don't overwrite real value with "***" if apiKey, ok := updates["api_key"]; ok { if s, _ := apiKey.(string); s == "***" || s == "" { @@ -415,6 +408,9 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R if displayName, ok := updates["display_name"].(string); ok { candidate.DisplayName = displayName } + if pt, ok := updates["provider_type"].(string); ok && pt != "" { + candidate.ProviderType = pt + } if settings, ok := updates["settings"]; ok { rawSettings, err := marshalJSONRaw(settings) if err != nil { @@ -424,6 +420,11 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R candidate.Settings = rawSettings } + // Re-validate URLs against the (possibly new) provider type. + // When provider_type changes, existing api_base must also pass validation + // for the new type — prevents SSRF via ACP→non-ACP type switch. + typeChanged := candidate.ProviderType != currentProvider.ProviderType + if apiBase, ok := updates["api_base"]; ok { if s, _ := apiBase.(string); s != "" { if err := validateProviderURL(s, candidate.ProviderType); err != nil { @@ -431,6 +432,11 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R return } } + } else if typeChanged && candidate.APIBase != "" { + if err := validateProviderURL(candidate.APIBase, candidate.ProviderType); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } } if baseURL, ok := updates["base_url"]; ok { if s, _ := baseURL.(string); s != "" {