Overview
SSRF vulnerabilities in Echo (Go) can allow an attacker to trigger the server to perform HTTP requests to arbitrary endpoints. When the server proxies or fetches remote resources based on user input, an attacker can cause the backend to reach internal services, cloud metadata endpoints, or other protected networks. This can facilitate data exfiltration, port scanning, or access to services that the server is authorized to reach, often bypassing protective network boundaries. No CVEs are referenced in this guide.
In Echo, SSRF tends to appear when handlers take a user-supplied URL (for example, image fetchers, content proxies, webhook validators, or resource fetch endpoints) and pass it directly to net/http calls. If the code does not validate schemes, hosts, or IP addresses, it can be exploited to access internal endpoints or take advantage of misconfigured network proxies. The risk is amplified when requests are performed with elevated privileges or in cloud environments where metadata endpoints or internal services are reachable from the server.
This guide outlines practical, framework-specific remediations for Echo apps, focusing on input validation, IP/hostname allowlisting, restricted outbound clients, and safer architectural patterns (e.g., avoiding arbitrary URL fetches or replacing them with signed, pre-approved fetch operations). It does not reference any CVEs but covers common SSRF patterns seen in Echo-based services.
Code Fix Example
Echo API Security Remediation
package main
import (
"io"
"net/http"
"net/url"
"time"
"net"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Vulnerable: uses user-supplied URL directly
e.GET("/ssrf/vuln", func(c echo.Context) error {
target := c.QueryParam("url")
resp, err := http.Get(target)
if err != nil {
return c.String(http.StatusBadRequest, "fetch error")
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return c.String(http.StatusOK, string(b))
})
// Fixed: validate and restrict outgoing requests
e.GET("/ssrf/fix", func(c echo.Context) error {
raw := c.QueryParam("url")
u, err := url.Parse(raw)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return c.String(http.StatusBadRequest, "invalid url")
}
host := u.Hostname()
ips, _ := net.LookupIP(host)
for _, ip := range ips {
if isPrivateIP(ip) {
return c.String(http.StatusBadRequest, "private IPs not allowed")
}
}
client := &http.Client{
Timeout: 5 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
resp, err := client.Get(raw)
if err != nil {
return c.String(http.StatusBadRequest, "fetch error")
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return c.String(http.StatusOK, string(b))
})
e.Start(":8080")
}
func isPrivateIP(ip net.IP) bool {
if ip4 := ip.To4(); ip4 != nil {
private4 := []net.IPNet{
{IP: net.IPv4(10,0,0,0), Mask: net.CIDRMask(8, 32)},
{IP: net.IPv4(172,16,0,0), Mask: net.CIDRMask(12, 32)},
{IP: net.IPv4(192,168,0,0), Mask: net.CIDRMask(16, 32)},
{IP: net.IPv4(127,0,0,0), Mask: net.CIDRMask(8, 32)},
}
for _, n := range private4 {
if n.Contains(ip4) { return true }
}
return false
}
private6 := []net.IPNet{
{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
}
for _, n := range private6 {
if n.Contains(ip) { return true }
}
return false
}