SSRF

SSRF and Go (Gin) remediation [CVE-2026-35587]

[Updated 2026-04] Updated CVE-2026-35587

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()
}

CVE References

Choose which optional cookies to allow. You can change this any time.