SSRF

SSRF in Go (Gin): CVE-2026-40346 [CVE-2026-40346]

[Updated Apr 2026] Updated CVE-2026-40346

Overview

The CVE-2026-40346 entry describes a server-side request forgery (SSRF) vulnerability in NocoBase, where an authenticated user could cause the backend to perform HTTP requests to URLs supplied by the user via the workflow HTTP request plugin and the custom request action plugin. This allowed access to internal network services, cloud metadata endpoints, and even localhost, illustrating CWE-918: Server-Side Request Forgery. The issue was patched in version 2.0.37. In Go (Gin) terms, this class of vulnerability arises when a handler accepts a user-provided URL and fetches it directly with a default HTTP client without validation, effectively turning your service into an unintended proxy. Attackers can pivot to internal resources or metadata endpoints, leading to data leakage or further compromise. The real-world impact mirrors the NocoBase scenario, underscoring why SSRF protections are essential in any web service that makes server-side requests based on client input. 3-4 paragraphs describe the real-world impact and how such patterns manifest in Go (Gin), referencing CVE-2026-40346 and CWE-918 specifically.

Affected Versions

NocoBase prior to 2.0.37 (CVE-2026-40346)

Code Fix Example

Go (Gin) API Security Remediation
package main

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

func main() {
  r := gin.Default()
  r.GET("/vuln", vulnerableHandler)
  r.GET("/fix", fixedHandler)
  r.Run(":8080")
}

func vulnerableHandler(c *gin.Context) {
  raw := c.Query("url")
  if raw == "" {
    c.String(400, "url param required")
    return
  }
  resp, err := http.Get(raw)
  if err != nil {
    c.String(502, err.Error())
    return
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)
  c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}

func fixedHandler(c *gin.Context) {
  raw := c.Query("url")
  if raw == "" {
    c.String(400, "url param required")
    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 !isAllowedHost(host) {
    c.String(403, "host not allowed")
    return
  }
  ips, err := net.LookupIP(host)
  if err != nil {
    c.String(400, "host lookup failed")
    return
  }
  for _, ip := range ips {
    if isPrivateIP(ip) {
      c.String(400, "target resolves to private/internal IPs")
      return
    }
  }
  tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}}
  client := &http.Client{Transport: tr, Timeout: 5 * time.Second}
  req, _ := http.NewRequest("GET", raw, nil)
  resp, err := client.Do(req)
  if err != nil {
    c.String(502, err.Error())
    return
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)
  c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}

func isAllowedHost(host string) bool {
  allowed := []string{"example.com", "api.internal", "localhost"}
  for _, a := range allowed {
    if strings.EqualFold(host, a) {
      return true
    }
  }
  return false
}

func isPrivateIP(ip net.IP) bool {
  if ip4 := ip.To4(); ip4 != nil {
    switch ip4[0] {
    case 10:
      return true
    case 127:
      return true
    case 172:
      if ip4[1] >= 16 && ip4[1] <= 31 {
        return true
      }
    case 192:
      if ip4[1] == 168 {
        return true
      }
    }
    return false
  }
  // IPv6 private ranges
  if ip.IsLoopback() {
    return true
  }
  if ip[0] == 0xfc || ip[0] == 0xfd {
    return true
  }
  if ip[0] == 0xfe && (ip[1]&0xc0) == 0x80 {
    return true
  }
  return false
}

CVE References

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