Introduction Link to heading
Server-Side Request Forgery (SSRF) is a security vulnerability that enables an attacker to make unauthorized server-to-server requests. Existing preventive measures in Go may not suffice due to challenges such as DNS-rebinding. This blog post explores the use of an allow-list mechanism to neutralize SSRF risks in Go, sidestepping vulnerabilities from parsing or DNS-rebinding attacks.
Why Traditional Methods Fall Short in Go Link to heading
Standard SSRF countermeasures, such as URL validation or DNS-record checks, are vulnerable to DNS-rebinding or URL parsing tricks. This makes a more robust protective measure essential.
The Allow-List Solution Link to heading
An allow-list mechanism permits only outbound HTTP requests to predetermined IP address ranges that are essential for your application. This is implemented by customizing Go’s native DialContext
function within the HTTP Transport. Before connecting, the DNS of the target host is resolved and matched against an approved list of IPs. This IP is then utilized for the actual connection, negating Time-of-Check to Time-of-Use (TOCTTOU) attacks, also known as DNS-rebinding.
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"time"
)
// AllowList offers way of doing SSRF-safe HTTP requests by creating a custom DialContext function based on an allow-list configuration
type AllowList struct {
Values []*net.IPNet // A custom list of IP ranges to allow.
}
var (
al = AllowList{
Values: []*net.IPNet{
{IP: net.ParseIP("1.1.1.1"), Mask: net.CIDRMask(32, 32)}, // IPv4 example
{IP: net.ParseIP("aaaa::"), Mask: net.CIDRMask(7, 128)}, // IPv6 example
},
}
)
func main() {
client := &http.Client{
Transport: &http.Transport{
DialContext: al.DialContext(),
},
Timeout: time.Duration(5 * time.Second),
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s", os.Args[1]), nil)
if err != nil {
log.Fatal(err)
}
_, err = client.Do(req)
if err != nil {
log.Fatal(err)
}
}
// Allowed returns whether or not the given IP is allowed by the configuration specified.
func (al AllowList) Allowed(IP net.IP) bool {
for _, r := range al.Values {
if r.Contains(IP) {
return true
}
}
return false
}
// DialContext returns a custom allow-list based DialContext, which blocks based on the configuration specified.
func (al AllowList) DialContext() func(ctx context.Context, network, addr string) (net.Conn, error) {
return func(_ context.Context, network string, addr string) (conn net.Conn, err error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
IPs, err := net.LookupHost(host)
if err != nil {
return nil, err
}
for _, v := range IPs {
IP := net.ParseIP(v)
if !al.Allowed(IP) {
err = errors.New("IP not allowed")
return
}
conn, err = net.Dial(network, net.JoinHostPort(v, port))
if err == nil {
break
}
}
return
}
}
In Action Link to heading
Running this script will yield the following responses:
003random:ssrf-allow-list/ $ go run main.go http://127.0.0.1:80
2023/10/01 23:19:57 Get "http://127.0.0.1:80": IP not allowed
exit status 1
003random:ssrf-allow-list/ $ go run main.go http://[::1]
2023/10/01 23:20:04 Get "http://[::1]": IP not allowed
exit status 1
003random:ssrf-allow-list/ $ go run main.go http://google.com
2023/10/01 23:20:11 Get "http://google.com": IP not allowed
exit status 1
003random:ssrf-allow-list/ $ go run main.go http://user:[email protected]
2023/10/01 23:20:25 Get "http://user:***@google.com": IP not allowed
exit status 1
Benefits Link to heading
- Selective Permitting: The allow-list approach authorizes a specific set of IPs at the lowest level of the request, ensuring a safe IP is also the requested one.
- Ease of Implementation: The solution is straightforward to deploy in Go without requiring third-party libraries.
Considerations Link to heading
- Allow-list Limitations: The opposite of a deny-list, an allow-list could restrict valid connections if not kept up-to-date. It’s crucial to manage the allow-list carefully to prevent false negatives.