diff --git a/util/ssrf.go b/util/ssrf.go
new file mode 100644
index 00000000..3e928af9
--- /dev/null
+++ b/util/ssrf.go
@@ -0,0 +1,128 @@
+/*
+ * Written in 2019 by Andrew Ayer.
+ * Patched 2025, Bluesky Social PBC.
+ *
+ * Original: https://www.agwa.name/blog/post/preventing_server_side_request_forgery_in_golang
+ *
+ * To the extent possible under law, the author(s) have dedicated all
+ * copyright and related and neighboring rights to this software to the
+ * public domain worldwide. This software is distributed without any
+ * warranty.
+ *
+ * You should have received a copy of the CC0 Public
+ * Domain Dedication along with this software. If not, see
+ * .
+ */
+package util
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "syscall"
+ "time"
+)
+
+func ipv4Net(a, b, c, d byte, subnetPrefixLen int) net.IPNet {
+ return net.IPNet{
+ IP: net.IPv4(a, b, c, d),
+ Mask: net.CIDRMask(96+subnetPrefixLen, 128),
+ }
+}
+
+var reservedIPv4Nets = []net.IPNet{
+ ipv4Net(0, 0, 0, 0, 8), // Current network
+ ipv4Net(10, 0, 0, 0, 8), // Private
+ ipv4Net(100, 64, 0, 0, 10), // RFC6598
+ ipv4Net(127, 0, 0, 0, 8), // Loopback
+ ipv4Net(169, 254, 0, 0, 16), // Link-local
+ ipv4Net(172, 16, 0, 0, 12), // Private
+ ipv4Net(192, 0, 0, 0, 24), // RFC6890
+ ipv4Net(192, 0, 2, 0, 24), // Test, doc, examples
+ ipv4Net(192, 88, 99, 0, 24), // IPv6 to IPv4 relay
+ ipv4Net(192, 168, 0, 0, 16), // Private
+ ipv4Net(198, 18, 0, 0, 15), // Benchmarking tests
+ ipv4Net(198, 51, 100, 0, 24), // Test, doc, examples
+ ipv4Net(203, 0, 113, 0, 24), // Test, doc, examples
+ ipv4Net(224, 0, 0, 0, 4), // Multicast
+ ipv4Net(240, 0, 0, 0, 4), // Reserved (includes broadcast / 255.255.255.255)
+}
+
+var globalUnicastIPv6Net = net.IPNet{
+ IP: net.IP{0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Mask: net.CIDRMask(3, 128),
+}
+
+func isIPv6GlobalUnicast(address net.IP) bool {
+ return globalUnicastIPv6Net.Contains(address)
+}
+
+func isIPv4Reserved(address net.IP) bool {
+ for _, reservedNet := range reservedIPv4Nets {
+ if reservedNet.Contains(address) {
+ return true
+ }
+ }
+ return false
+}
+
+func IsPublicIPAddress(address net.IP) bool {
+ if address.To4() != nil {
+ return !isIPv4Reserved(address)
+ } else {
+ return isIPv6GlobalUnicast(address)
+ }
+}
+
+// Implementation of [net.Dialer] `Control` field (a function) which avoids some SSRF attacks by rejecting local IPv4 and IPv6 address ranges, and only allowing ports 80 or 443.
+func PublicOnlyControl(network string, address string, conn syscall.RawConn) error {
+ if !(network == "tcp4" || network == "tcp6") {
+ return fmt.Errorf("%s is not a safe network type", network)
+ }
+
+ host, port, err := net.SplitHostPort(address)
+ if err != nil {
+ return fmt.Errorf("%s is not a valid host/port pair: %s", address, err)
+ }
+
+ ipaddress := net.ParseIP(host)
+ if ipaddress == nil {
+ return fmt.Errorf("%s is not a valid IP address", host)
+ }
+
+ if !IsPublicIPAddress(ipaddress) {
+ return fmt.Errorf("%s is not a public IP address", ipaddress)
+ }
+
+ if !(port == "80" || port == "443") {
+ return fmt.Errorf("%s is not a safe port number", port)
+ }
+
+ return nil
+}
+
+// [net.Dialer] with [PublicOnlyControl] for `Control` function (for SSRF protection). Other fields are same default values as standard library.
+func PublicOnlyDialer() *net.Dialer {
+ return &net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ DualStack: true,
+ Control: PublicOnlyControl,
+ }
+}
+
+// [http.Transport] with [PublicOnlyDialer] for `DialContext` field (for SSRF protection). Other fields are same default values as standard library.
+//
+// Use this in an [http.Client] like: `c := http.Client{ Transport: PublicOnlyTransport() }`
+func PublicOnlyTransport() *http.Transport {
+ dialer := PublicOnlyDialer()
+ return &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: dialer.DialContext,
+ ForceAttemptHTTP2: true,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ }
+}
diff --git a/util/ssrf_test.go b/util/ssrf_test.go
new file mode 100644
index 00000000..2e987024
--- /dev/null
+++ b/util/ssrf_test.go
@@ -0,0 +1,32 @@
+package util
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPublicOnlyTransport(t *testing.T) {
+ t.Skip("skipping local SSRF test")
+ assert := assert.New(t)
+
+ c := http.Client{
+ Transport: PublicOnlyTransport(),
+ }
+
+ {
+ _, err := c.Get("http://127.0.0.1:2470/")
+ assert.Error(err)
+ }
+
+ {
+ _, err := c.Get("http://localhost:2470/path")
+ assert.Error(err)
+ }
+
+ {
+ _, err := c.Get("http://bsky.app:8080/path")
+ assert.Error(err)
+ }
+}
--
2.51.2
From b5d04fb2a996e89e88ddec78dc72884299c3a171 Mon Sep 17 00:00:00 2001
From: bryan newbold
Date: Tue, 29 Apr 2025 15:35:23 -0700
Subject: [PATCH 2/3] move SSRF code to sub-package of util
---
util/{ => ssrf}/ssrf.go | 4 ++--
util/{ => ssrf}/ssrf_test.go | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
rename util/{ => ssrf}/ssrf.go (99%)
rename util/{ => ssrf}/ssrf_test.go (97%)
diff --git a/util/ssrf.go b/util/ssrf/ssrf.go
similarity index 99%
rename from util/ssrf.go
rename to util/ssrf/ssrf.go
index 3e928af9..77a718e2 100644
--- a/util/ssrf.go
+++ b/util/ssrf/ssrf.go
@@ -13,7 +13,7 @@
* Domain Dedication along with this software. If not, see
* .
*/
-package util
+package ssrf
import (
"fmt"
@@ -51,7 +51,7 @@ var reservedIPv4Nets = []net.IPNet{
var globalUnicastIPv6Net = net.IPNet{
IP: net.IP{0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Mask: net.CIDRMask(3, 128),
-}
+}t G
func isIPv6GlobalUnicast(address net.IP) bool {
return globalUnicastIPv6Net.Contains(address)
diff --git a/util/ssrf_test.go b/util/ssrf/ssrf_test.go
similarity index 97%
rename from util/ssrf_test.go
rename to util/ssrf/ssrf_test.go
index 2e987024..a01e802d 100644
--- a/util/ssrf_test.go
+++ b/util/ssrf/ssrf_test.go
@@ -1,4 +1,4 @@
-package util
+package ssrf
import (
"net/http"
--
2.51.2
From 16f1b3dd1a2e1ba87bceeac8a097cd0a925e1941 Mon Sep 17 00:00:00 2001
From: bryan newbold
Date: Tue, 29 Apr 2025 15:36:03 -0700
Subject: [PATCH 3/3] fix stray typo
---
util/ssrf/ssrf.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/util/ssrf/ssrf.go b/util/ssrf/ssrf.go
index 77a718e2..3ebf9698 100644
--- a/util/ssrf/ssrf.go
+++ b/util/ssrf/ssrf.go
@@ -51,7 +51,7 @@ var reservedIPv4Nets = []net.IPNet{
var globalUnicastIPv6Net = net.IPNet{
IP: net.IP{0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Mask: net.CIDRMask(3, 128),
-}t G
+}
func isIPv6GlobalUnicast(address net.IP) bool {
return globalUnicastIPv6Net.Contains(address)