fix(security): block SSRF via custom AI model URL

Apply security.ValidateURL() to custom_api_url in PUT /api/models before
storing — blocks private IPs, cloud metadata endpoints, and localhost.
Replace plain http.Client in mcp/config.go with security.SafeHTTPClient()
for defense-in-depth (DialContext blocks private IPs, CheckRedirect
validates targets). Add SSRF warning to WithHTTPClient() docs.
This commit is contained in:
tinkle-community
2026-03-10 00:13:44 +08:00
parent 8406f2f998
commit 7b9a0740c1
3 changed files with 17 additions and 2 deletions

View File

@@ -12,6 +12,7 @@ import (
"nofx/crypto" "nofx/crypto"
"nofx/logger" "nofx/logger"
"nofx/manager" "nofx/manager"
"nofx/security"
"nofx/market" "nofx/market"
"nofx/provider/alpaca" "nofx/provider/alpaca"
"nofx/provider/coinank/coinank_api" "nofx/provider/coinank/coinank_api"
@@ -1770,6 +1771,15 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
// Update each model's configuration and track traders that need reload // Update each model's configuration and track traders that need reload
tradersToReload := make(map[string]bool) tradersToReload := make(map[string]bool)
for modelID, modelData := range req.Models { for modelID, modelData := range req.Models {
// SSRF protection: validate custom_api_url before storing
if modelData.CustomAPIURL != "" {
cleanURL := strings.TrimSuffix(modelData.CustomAPIURL, "#")
if err := security.ValidateURL(cleanURL); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid custom_api_url for model %s: %s", modelID, err.Error())})
return
}
}
// Find traders using this AI model BEFORE updating // Find traders using this AI model BEFORE updating
traders, _ := s.store.Trader().ListByAIModelID(userID, modelID) traders, _ := s.store.Trader().ListByAIModelID(userID, modelID)
for _, t := range traders { for _, t := range traders {

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"nofx/logger" "nofx/logger"
"nofx/security"
) )
// Config client configuration (centralized management of all configurations) // Config client configuration (centralized management of all configurations)
@@ -48,7 +49,7 @@ func DefaultConfig() *Config {
// Default dependencies (use global logger) // Default dependencies (use global logger)
Logger: logger.NewMCPLogger(), Logger: logger.NewMCPLogger(),
HTTPClient: &http.Client{Timeout: DefaultTimeout}, HTTPClient: security.SafeHTTPClient(DefaultTimeout),
} }
} }

View File

@@ -22,7 +22,11 @@ func WithLogger(logger Logger) ClientOption {
} }
} }
// WithHTTPClient sets custom HTTP client // WithHTTPClient sets custom HTTP client.
//
// WARNING: The default client uses security.SafeHTTPClient() with SSRF protection
// (blocks private IPs, cloud metadata, validates redirects). Overriding it bypasses
// these protections. Only use in tests or with a client providing equivalent safeguards.
// //
// Usage example: // Usage example:
// httpClient := &http.Client{Timeout: 60 * time.Second} // httpClient := &http.Client{Timeout: 60 * time.Second}