flawopen.com/ssrf/Go

● CWE-918 · Alta
Pesquisa · 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.

💡 Explicação em Linguagem Simples (ELI5)

Imagine enviar um assistente de escritório para buscar um pacote em uma loja pública, mas fornecendo o endereço do cofre trancado na sala da gerência. Como o assistente tem um crachá interno, ele abre o cofre e entrega segredos da empresa.

Conceitos Centrais e Termos

Web Application Security
Componente de arquitetura principal afetado pelo CWE-918.
CWE-918
Classificação padrão Common Weakness Enumeration (CWE) para ssrf-go.
Defense-in-Depth
Verificação de engenharia em múltiplas camadas e isolamento de limites em tempo de execução.

Fluxo de Ataque Passo a Paso

Step 1

Ingestão de URL Remota Não Confiável

O aplicativo recebe uma URL de usuário para baixar webhooks, buscar avatares ou gerar relatórios PDF.

Step 2

Endereçamento de Rede Interna

O invasor fornece um endereço local ou do serviço de metadados de nuvem (ex: http://169.254.169.254/latest/meta-data/).

Step 3

Egress de Soquete Irrestrito

O cliente HTTP do servidor inicia a conexão de dentro da VPC privada sem validar o IP resolvido ou sub-rede.

Step 4

Exfiltração de Identidade e Credenciais em Nuvem

O serviço interno de metadados confia no IP do servidor e retorna credenciais temporárias de IAM ou tokens de acesso.

Código-Fonte: Vulnerável vs. Seguro

✕ IMPLEMENTAÇÃO VULNERÁVEL
// 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)
}
✓ PATCH SEGURO E ROBUSTO
// 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)
}

Lista de Verificação de Segurança para Engenharia

References