SSRF

SSRF in Go Gin: Remediation Guide [Jun 2024] [CVE-2026-35409]

[Updated Jun 2024] Updated CVE-2026-35409

Overview

A server that fetches resources based on user-supplied URLs can be forced to access internal services or metadata endpoints (for example, cloud provider metadata, internal dashboards, or internal services) by an attacker. This SSRF risk is particularly dangerous in Go (Gin) applications because handlers frequently perform outbound HTTP requests using net/http directly from request parameters without validating the destination. In cloud environments, this may enable an attacker to reach the instance metadata service, bypass firewalls, or port-scan an internal network. The impact include information disclosure, unintended access to internal services, and potential abuse of the server as a proxy for broader attacks. While SSRF is not unique to Gin, the framework is often used for building HTTP APIs that process user input and perform server-side fetches, making this class of vulnerability highly relevant to Gin-based services. This guide presents practical, framework-aware remediation steps for Go (Gin) applications, focusing on validating user-controlled URLs, restricting destinations, and configuring the HTTP client to minimize exposure. This guidance does not reference specific CVEs since none are provided in this request.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "io"
  "net"
  "net/http"
  "net/url"
  "time"
  "github.com/gin-gonic/gin"
)

func fetchVulnerable(c *gin.Context) {
  target := c.Query(`url`)
  if len(target) == 0 {
    c.JSON(400, gin.H{`error`: `url query parameter is required`})
    return
  }
  resp, err := http.Get(target)
  if err != nil {
    c.JSON(500, gin.H{`error`: err.Error()})
    return
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)
  c.Data(resp.StatusCode, `text/plain`, body)
}

func fetchSafe(c *gin.Context) {
  target := c.Query(`url`)
  if len(target) == 0 {
    c.JSON(400, gin.H{`error`: `url query parameter is required`})
    return
  }
  u, err := url.Parse(target)
  if err != nil || (u.Scheme != `http` && u.Scheme != `https`) {
    c.JSON(400, gin.H{`error`: `invalid or unsupported URL`})
    return
  }
  allowed := map[string]bool{
    `example.com`:     true,
    `api.example.com`: true,
  }
  host := u.Hostname()
  if !allowed[host] {
    c.JSON(400, gin.H{`error`: `host not allowed`})
    return
  }
  ips, err := net.LookupIP(host)
  if err != nil || len(ips) == 0 {
    c.JSON(500, gin.H{`error`: `DNS resolution failed`})
    return
  }
  for _, ip := range ips {
    if ip.IsPrivate() || ip.IsLoopback() {
      c.JSON(400, gin.H{`error`: `private/loopback IPs not allowed`})
      return
    }
  }
  client := &http.Client{
    Timeout: 5 * time.Second,
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
      if len(via) >= 3 {
        return http.ErrUseLastResponse
      }
      return nil
    },
  }
  resp, err := client.Get(target)
  if err != nil {
    c.JSON(500, gin.H{`error`: err.Error()})
    return
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)
  c.Data(resp.StatusCode, `text/plain`, body)
}

func main() {
  r := gin.Default()
  r.GET(`/vuln`, fetchVulnerable)
  r.GET(`/safe`, fetchSafe)
  r.Run(`:8080`)
}

CVE References

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