THN Interview Prep

SSRF & Controlled Outbound HTTP

Server-Side Request Forgery (SSRF) occurs when a web application fetches a remote resource without validating the user-supplied URL. An attacker can coerce the application to send crafted requests to internal-only systems, cloud metadata services, or third-party APIs.


Core details

LayerDefensesImpact
NetworkEgress firewalls, default-deny, dedicated egress proxyPrevents lateral movement to internal VPC resources
DNSDNS Pinning / pre-resolution validationDefeats DNS Rebinding exploits
ApplicationHostname allowlists, scheme restrictions (https only)Blocks protocol smuggling (e.g., file://, gopher://, dict://)
Cloud InfrastructureEnforce IMDSv2 (session token header, hop limit = 1)Secures AWS/GCP/Azure instance metadata endpoints

The DNS Rebinding Trap

A simple hostname check is vulnerable to Time-of-Check to Time-of-Use (TOCTOU) DNS Rebinding. During validation, the hostname resolves to a benign public IP. When the client makes the actual HTTP fetch, the DNS server returns a private IP (e.g., 127.0.0.1 or 169.254.169.254).

[!IMPORTANT] To prevent DNS Rebinding, you must resolve the hostname first, validate that the resolved IP address is public and not within private CIDR ranges (RFC 1918 / RFC 4193), and then establish the connection directly to the validated IP, passing the original hostname in the Host header for TLS SNI validation.


Understanding

When an application performs outbound requests (e.g., webhook delivery, PDF generation, image scraping), it is acting as a proxy. Naive implementations trust standard client libraries (e.g., Axios, fetch, or http.Get), which automatically follow redirects and resolve DNS records using the system resolver without boundaries.

If an attacker inputs http://169.254.169.254/latest/meta-data/ on an AWS-hosted server, they can retrieve temporary IAM credentials. If they use http://localhost:6379/, they can send arbitrary commands to Redis (protocol smuggling via HTTP headers).

Secure Egress Topology

The most robust architecture separates the application layer from the egress routing. All application outbound requests are forced through a secure egress proxy (like Squid, Envoy, or Smokescreen) which maintains strict IP and domain allowlists.


Senior understanding

Secure HTTP Client Implementation

Go Implementation

Using a custom dialer to validate resolved IPs before establishing a TCP socket:

package main

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

func isPrivateIP(ip net.IP) bool {
	return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate()
}

func NewSecureClient() *http.Client {
	dialer := &net.Dialer{
		Timeout:   5 * time.Second,
		KeepAlive: 30 * 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
			}

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

			for _, ip := range ips {
				if isPrivateIP(ip) {
					return nil, errors.New("forbidden target: private IP detected")
				}
			}

			// Force connection to the first resolved public IP to prevent DNS rebinding
			targetAddr := net.JoinHostPort(ips[0].String(), port)
			return dialer.DialContext(ctx, network, targetAddr)
		},
		MaxIdleConns:          100,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   5 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
	}

	return &http.Client{
		Transport: transport,
		Timeout:   10 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) >= 3 {
				return errors.New("stopped after 3 redirects")
			}
			return nil // The custom DialContext will re-validate the redirect destination IP
		},
	}
}

Diagram

Loading diagram…

See also

Mark this page when you finish learning it.

Spotted something unclear or wrong on this page?

On this page