flawopen.com/ssrf/Go

● CWE-918 · 高危
安全研究 · FlawOpen

漏洞深度剖析:Server-Side Request Forgery in Go

How http.Get in Go allows internal network traversal, and how to build safe http.Client transports with DialContext.

💡 通俗易懂的原理解析 (ELI5)

想象一下,你派办公室助理去公共快递站取包裹,却故意给他留了总经理办公室带锁保险箱的内部地址。因为助理佩戴着公司内部通行工牌,保安直接放行,他便打开保险箱将公司核心机密交给了你。

核心概念与专有名词

Web Application Security
技术概念 (Web Application Security):Core architecture component affected by CWE-918.
CWE-918
技术概念 (CWE-918):Standard Common Weakness Enumeration classification for ssrf-go.
Defense-in-Depth
技术概念 (Defense-in-Depth):Multi-layered engineering verification and runtime boundary isolation.

攻击执行流程分解

Step 1

接收不可信远程 URL

应用程序接受用户提交的外部 URL 用于拉取头像、Webhook 推送或生成网页预览。

Step 2

指向内部专用网络与元数据服务

攻击者输入指向内网回环地址或云厂商元数据接口(如 http://169.254.169.254/latest/meta-data/)的地址。

Step 3

未受限制的内部网络套接字请求

服务器内部 HTTP 客户端直接从受信任的私有 VPC 发起网络请求,未做 IP 白名单与私有地址段校验。

Step 4

云身份凭证与敏感数据外泄

内部元数据服务信任来自同主机的请求,回传临时 IAM 访问凭据、Kubernetes Token 或管理后台内容。

源代码对比:漏洞与安全实现

✕ 存在漏洞的实现
// VULNERABLE: Default http.Get allows internal network calls
package main

import (
	"io"
	"net/http"
)

func proxyHandler(w http.ResponseWriter, r *http.Request) {
	targetURL := r.URL.Query().Get("url")

	// Attacker passes: http://169.254.169.254/latest/meta-data/
	resp, err := http.Get(targetURL)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	io.Copy(w, resp.Body)
}
✓ 加固后的安全修复
// HARDENED: Custom DialContext checking net.IP.IsPrivate and IsLoopback
package main

import (
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"net/url"
	"time"
)

func isSafeIP(ip net.IP) bool {
	return !ip.IsLoopback() &&
		!ip.IsPrivate() &&
		!ip.IsLinkLocalUnicast() &&
		!ip.IsLinkLocalMulticast() &&
		!ip.IsUnspecified()
}

func newSafeHTTPClient() *http.Client {
	dialer := &net.Dialer{
		Timeout: 3 * time.Second,
	}

	transport := &http.Transport{
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			host, port, err := net.SplitHostPort(addr)
			if err != nil {
				return nil, err
			}

			// Resolve all IP addresses
			ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
			if err != nil {
				return nil, err
			}

			for _, ip := range ips {
				if !isSafeIP(ip) {
					return nil, errors.New("SSRF Blocked: connection to private/internal IP rejected")
				}
			}

			// Connect to the first validated IP
			targetAddr := net.JoinHostPort(ips[0].String(), port)
			return dialer.DialContext(ctx, network, targetAddr)
		},
	}

	return &http.Client{
		Transport: transport,
		Timeout:   5 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return errors.New("redirects are disabled to prevent SSRF bypass")
		},
	}
}

func proxyHandler(w http.ResponseWriter, r *http.Request) {
	targetURL := r.URL.Query().Get("url")
	parsed, err := url.Parse(targetURL)
	if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
		http.Error(w, "Invalid URL scheme", http.StatusBadRequest)
		return
	}

	client := newSafeHTTPClient()
	resp, err := client.Get(targetURL)
	if err != nil {
		http.Error(w, "Request failed: "+err.Error(), http.StatusForbidden)
		return
	}
	defer resp.Body.Close()

	io.Copy(w, resp.Body)
}

工程与系统安全加固清单

References