Overview
SSRF (Server-Side Request Forgery) occurs when a server-side component fetches a URL supplied by a client or config without validating the target. In CVE-2026-35587, Glances exposed an SSRF path by passing a user-modifiable public_api value directly into an outbound HTTP request, with no scheme/host validation and with credentials potentially leaking in Authorization headers when public_username/public_password were set. This allowed attackers to reach arbitrary internal or external endpoints, including internal services or cloud metadata endpoints, and even exfiltrate credentials. Although this CVE is tied to Glances and CWE-918, the core lesson-unvalidated outbound requests from server components-applies broadly. In Go (Gin) apps, SSRF naturally arises when an endpoint consumes a URL from a client or from config and uses net/http to fetch it without strict validation. If such requests carry user-supplied credentials, those can also leak to attacker-controlled servers. The remediation is to validate and constrain outbound destinations, and to avoid propagating user-controlled credentials in outbound requests.
Code Fix Example
Go (Gin) API Security Remediation
Vulnerable pattern (Go Gin, unvalidated user-provided URL, potential credential forwarding):
package main
import (
"io/ioutil"
"net/http"
"github.com/gin-gonic/gin"
)
type Config struct {
PublicAPI string
PublicUsername string
PublicPassword string
}
func main() {
r := gin.Default()
cfg := Config{PublicAPI: ""}
r.GET("/fetch", func(c *gin.Context) {
// URL comes directly from query parameter and is used without validation
target := c.Query("url")
if target == "" {
c.String(400, "url required")
return
}
// Vulnerable: outbound request to user-controlled URL
req, _ := http.NewRequest("GET", target, nil)
// Potential credential leakage: using user-provided credentials in header
if cfg.PublicUsername != "" && cfg.PublicPassword != "" {
req.SetBasicAuth(cfg.PublicUsername, cfg.PublicPassword)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
c.String(500, err.Error())
return
}
defer resp.Body.Close()
b, _ := ioutil.ReadAll(resp.Body)
c.Data(200, "text/plain", b)
})
r.Run()
}
Fixed pattern (Go Gin, validated URL, no user-supplied credentials, safer outbound):
package main
import (
"io"
"net/http"
"net/url"
"time"
"net"
"strings"
"fmt"
"io"
"github.com/gin-gonic/gin"
)
type Config struct {
PublicAPI string
PublicUsername string
PublicPassword string
}
func isPrivateHost(host string) bool {
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip4 := ip.To4(); ip4 != nil {
b0 := ip4[0]
b1 := ip4[1]
// 10.0.0.0/8
if b0 == 10 {
return true
}
// 172.16.0.0/12
if b0 == 172 && b1 >= 16 && b1 <= 31 {
return true
}
// 192.168.0.0/16
if b0 == 192 && ip4[1] == 168 {
return true
}
}
if host == "localhost" {
return true
}
return false
}
func isAllowedURL(raw string) bool {
u, err := url.Parse(raw)
if err != nil {
return false
}
if u.Scheme != "http" && u.Scheme != "https" {
return false
}
host := u.Hostname()
if host == "" {
return false
}
// Reject private/internal hosts to mitigate SSRF risk
if isPrivateHost(host) {
return false
}
return true
}
func fetchURL(cfg Config) ([]byte, error) {
if !isAllowedURL(cfg.PublicAPI) {
return nil, fmt.Errorf("URL not allowed: %s", cfg.PublicAPI)
}
client := &http.Client{ Timeout: 5 * time.Second }
req, err := http.NewRequest("GET", cfg.PublicAPI, nil)
if err != nil {
return nil, err
}
// Do NOT forward user-provided credentials; keep credentials in a secure store if needed
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("bad status: %s", resp.Status)
}
return io.ReadAll(resp.Body)
}
func main() {
r := gin.Default()
cfg := Config{PublicAPI: "https://example.com/api"}
r.GET("/fetch", func(c *gin.Context) {
data, err := fetchURL(cfg)
if err != nil {
c.String(500, err.Error())
return
}
c.Data(200, "application/octet-stream", data)
})
r.Run()
}