SSRF

SSRF in Go Gin: Secure Coding Guide [Apr 2026] [GHSA-cmcr-q4jf-p6q9]

[Updated Apr 2026] Updated GHSA-cmcr-q4jf-p6q9

Overview

SSRF (Server-Side Request Forgery) can force a vulnerable Go service to access arbitrary resources, potentially exposing internal networks or cloud metadata endpoints. In Go applications using Gin, a common pattern is to read a URL from client input and fetch it with the net/http client. If the URL is not validated, an attacker can trigger requests to internal services, sensitive ports, or the provider's metadata service. In Gin handlers, SSRF often occurs when user-provided URLs are proxied or fetched and the response is returned to the client. Without strict validation, allowed schemes, or destination allowlists, requests can escape the intended boundary and reach private infrastructure, triggering information disclosure or privilege escalation paths via subsequent interactions. Mitigation centers on validating and constraining outbound requests. Enforce an allowlist of safe destinations, require explicit schemes, and ensure the destination is not a private IP. Consider routing outbound traffic through a controlled proxy or gateway, and apply timeouts, redirects limits, and comprehensive logging. Architecture-wise, consolidate outbound HTTP handling into a single, well-audited wrapper that applies consistent checks and monitors anomalies. Regularly test endpoints for SSRF scenarios and maintain updated dependencies to reduce risk.

Code Fix Example

Go (Gin) API Security Remediation
package main

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

func main() {
  r := gin.Default()

  // Vulnerable pattern
  r.GET("/vulnerable", func(c *gin.Context) {
    target := c.Query("url")
    if target == "" {
      c.String(400, `missing url`)
      return
    }
    resp, err := http.Get(target)
    if err != nil {
      c.String(500, `request error: %v`, err)
      return
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    c.Data(resp.StatusCode, `text/plain`, body)
  })

  // Remediated pattern
  allowed := map[string]struct{}{
    `example.com`:     {},
    `api.example.org`: {},
  }
  r.GET("/fixed", func(c *gin.Context) {
    raw := c.Query("url")
    if raw == "" {
      c.String(400, `missing url`)
      return
    }
    u, err := url.Parse(raw)
    if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
      c.String(400, `invalid url`)
      return
    }
    host := u.Hostname()
    if _, ok := allowed[host]; !ok {
      c.String(403, `host not allowed`)
      return
    }
    resp, err := http.Get(raw)
    if err != nil {
      c.String(500, `request error: %v`, err)
      return
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    c.Data(resp.StatusCode, `text/plain`, body)
  })

  r.Run(":8080")
}

CVE References

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