flawopen.com/ssrf/Go

● CWE-918 · Tinggi
Riset Keamanan · 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.

💡 Penjelasan Sederhana (ELI5)

Bayangkan mengirim kurir kantor untuk mengambil paket di toko umum, tetapi memberinya alamat brankas terkunci di ruang manajer. Karena kurir memiliki lencana akses internal, dia membuka brankas dan menyerahkan rahasia perusahaan kepada Anda.

Konsep Kunci & Istilah

Web Application Security
Komponen arsitektur utama yang terpengaruh oleh CWE-918.
CWE-918
Klasifikasi standar Common Weakness Enumeration (CWE) untuk ssrf-go.
Defense-in-Depth
Verifikasi rekayasa berlapis dan isolasi batas waktu proses (runtime).

Alur Serangan Langkah demi Langkah

Step 1

Penerimaan URL Jarak Jauh Tidak Tepercaya

Aplikasi menerima parameter URL dari pengguna untuk mengunduh avatar, webhook, atau pratinjau dokumen.

Step 2

Pengalamatan Jaringan Internal

Penyerang memasukkan alamat loopback atau layanan metadata cloud (misal: http://169.254.169.254/latest/meta-data/).

Step 3

Koneksi Keluar Soket Tanpa Filter

Klien HTTP server mengirim permintaan dari dalam VPC privat tanpa memeriksa rentang subnet privat.

Step 4

Pencurian Kredensial Cloud IAM

Layanan metadata internal mempercayai permintaan dan mengembalikan token kredensial IAM sementara ke penyerang.

Kode Sumber: Rentan vs Aman

✕ IMPLEMENTASI RENTAN
// 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)
}
✓ PERBAIKAN AMAN & KUAT
// 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)
}

Daftar Periksa Penguatan Sistem Rekayasa

References