v2.31.0-dev8 ec8c5b9

ssrf.go RawHistoryFolder

1// Package proxy implements the reverse proxy use case.
2package proxy
3
4import (
5	"fmt"
6	"net"
7	"sync"
8
9	"github.com/bnema/gordon/internal/domain"
10)
11
12// blockedCIDRStrings contains CIDR blocks that should never be proxied to.
13// This prevents SSRF attacks targeting internal services or cloud metadata endpoints.
14var blockedCIDRStrings = []string{
15	// IPv4 loopback
16	"127.0.0.0/8",
17	// IPv4 private networks (RFC 1918)
18	"10.0.0.0/8",
19	"172.16.0.0/12",
20	"192.168.0.0/16",
21	// IPv4 link-local (cloud metadata endpoints like AWS/GCP/Azure)
22	"169.254.0.0/16",
23	// IPv4 localhost alternative
24	"0.0.0.0/8",
25	// IPv6 loopback
26	"::1/128",
27	// IPv6 unique local addresses (RFC 4193)
28	"fc00::/7",
29	// IPv6 link-local
30	"fe80::/10",
31}
32
33var (
34	blockedCIDRs     []*net.IPNet
35	blockedCIDRsOnce sync.Once
36)
37
38// initBlockedCIDRs parses the blocked CIDR strings into net.IPNet.
39// Called lazily on first use.
40func initBlockedCIDRs() {
41	blockedCIDRsOnce.Do(func() {
42		blockedCIDRs = make([]*net.IPNet, 0, len(blockedCIDRStrings))
43		for _, cidr := range blockedCIDRStrings {
44			_, network, err := net.ParseCIDR(cidr)
45			if err != nil {
46				// This should never happen with our hardcoded CIDRs
47				panic(fmt.Sprintf("invalid blocked CIDR: %s: %v", cidr, err))
48			}
49			blockedCIDRs = append(blockedCIDRs, network)
50		}
51	})
52}
53
54// isBlockedIP checks if an IP address is in any of the blocked CIDR ranges.
55func isBlockedIP(ip net.IP) bool {
56	initBlockedCIDRs()
57	for _, network := range blockedCIDRs {
58		if network.Contains(ip) {
59			return true
60		}
61	}
62	return false
63}
64
65// IsBlockedTarget checks if a target host is in a blocked network range.
66// It handles both IP addresses and hostnames (via DNS resolution).
67// Returns (blocked, error) where error indicates DNS resolution failure.
68//
69// SECURITY: This function is used to prevent SSRF attacks by blocking
70// requests to internal networks, localhost, and cloud metadata endpoints.
71func IsBlockedTarget(host string) (bool, error) {
72	initBlockedCIDRs()
73
74	// First, try to parse as IP directly
75	if ip := net.ParseIP(host); ip != nil {
76		return isBlockedIP(ip), nil
77	}
78
79	// Not an IP address, resolve DNS
80	ips, err := net.LookupIP(host)
81	if err != nil {
82		// DNS resolution failed - block by default for safety
83		// This prevents DNS rebinding attacks where an attacker controls
84		// a domain that initially resolves to a public IP, then later
85		// resolves to an internal IP.
86		return true, fmt.Errorf("DNS resolution failed for %s: %w", host, err)
87	}
88
89	if len(ips) == 0 {
90		// No IPs resolved - block for safety
91		return true, fmt.Errorf("no IP addresses found for %s", host)
92	}
93
94	// Check all resolved IPs - block if ANY resolve to a blocked network
95	for _, ip := range ips {
96		if isBlockedIP(ip) {
97			return true, nil
98		}
99	}
100
101	return false, nil
102}
103
104// ValidateExternalRouteTarget validates that an external route target
105// is safe to proxy to. Returns domain.ErrSSRFBlocked if the target
106// resolves to an internal/blocked network.
107func ValidateExternalRouteTarget(host string) error {
108	blocked, err := IsBlockedTarget(host)
109	if err != nil {
110		// DNS error - treat as blocked for security
111		return fmt.Errorf("%w: %v", domain.ErrSSRFBlocked, err)
112	}
113	if blocked {
114		return domain.ErrSSRFBlocked
115	}
116	return nil
117}
118
119// ResolveAndValidateHost resolves DNS for a hostname and returns the first
120// resolved IP that is not in a blocked CIDR range. If all resolved IPs are
121// blocked, returns ErrSSRFBlocked. For raw IP addresses, validates and returns
122// them directly.
123//
124// SECURITY: This function pins DNS resolution to prevent TOCTOU/DNS-rebinding
125// attacks where a hostname resolves to a public IP during validation but to a
126// private IP when the proxy actually connects.
127func ResolveAndValidateHost(host string) (string, error) {
128	initBlockedCIDRs()
129
130	// If already an IP address, validate and return directly
131	if ip := net.ParseIP(host); ip != nil {
132		if isBlockedIP(ip) {
133			return "", domain.ErrSSRFBlocked
134		}
135		return host, nil
136	}
137
138	// Resolve DNS
139	ips, err := net.LookupIP(host)
140	if err != nil {
141		return "", fmt.Errorf("%w: DNS resolution failed for %s: %v", domain.ErrSSRFBlocked, host, err)
142	}
143	if len(ips) == 0 {
144		return "", fmt.Errorf("%w: no IP addresses found for %s", domain.ErrSSRFBlocked, host)
145	}
146
147	// Find the first non-blocked IP
148	for _, ip := range ips {
149		if !isBlockedIP(ip) {
150			return ip.String(), nil
151		}
152	}
153
154	return "", domain.ErrSSRFBlocked
155}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close