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/)や内部ループバックURLを指定します。

Step 3

保護領域からの制限なきソケット送信

サーバーのHTTPクライアントがプライベートVPC内からリクエストを発行し、内部IPチェックを行いません。

Step 4

クラウド認証情報およびトークンの窃取

メタデータサービスがリクエストを信頼し、一時的なIAM認証情報や内部APIデータを攻撃者に返却します。

ソースコード比較:脆弱 vs 堅牢化

✕ 脆弱な実装
// 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