main bac21c6

ssrf_test.go RawHistoryFolder

1package proxy
2
3import (
4	"errors"
5	"net"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/gordon/internal/domain"
12)
13
14func TestIsBlockedTarget_IPAddresses(t *testing.T) {
15	tests := []struct {
16		name    string
17		host    string
18		blocked bool
19	}{
20		// IPv4 loopback - should block
21		{"IPv4 loopback 127.0.0.1", "127.0.0.1", true},
22		{"IPv4 loopback 127.0.0.2", "127.0.0.2", true},
23		{"IPv4 loopback 127.255.255.255", "127.255.255.255", true},
24
25		// IPv4 private networks - should block
26		{"10.0.0.0/8 network", "10.0.0.1", true},
27		{"10.0.0.0/8 network edge", "10.255.255.255", true},
28		{"172.16.0.0/12 network start", "172.16.0.1", true},
29		{"172.16.0.0/12 network middle", "172.20.0.1", true},
30		{"172.16.0.0/12 network end", "172.31.255.255", true},
31		{"192.168.0.0/16 network", "192.168.1.1", true},
32		{"192.168.0.0/16 network edge", "192.168.255.255", true},
33
34		// AWS/GCP/Azure metadata endpoint - should block
35		{"AWS metadata endpoint", "169.254.169.254", true},
36		{"Link-local address", "169.254.0.1", true},
37
38		// IPv6 loopback - should block
39		{"IPv6 loopback", "::1", true},
40
41		// IPv6 unique local - should block
42		{"IPv6 unique local fc00", "fc00::1", true},
43		{"IPv6 unique local fd00", "fd00::1", true},
44
45		// IPv6 link-local - should block
46		{"IPv6 link-local", "fe80::1", true},
47
48		// Public IPs - should allow
49		{"Google DNS", "8.8.8.8", false},
50		{"Cloudflare DNS", "1.1.1.1", false},
51		{"Public IP", "203.0.113.1", false},
52		{"Public IPv6", "2001:4860:4860::8888", false},
53
54		// Edge cases for 172.16.0.0/12
55		{"Just outside 172.16.0.0/12", "172.15.255.255", false},
56		{"Just outside 172.16.0.0/12 upper", "172.32.0.0", false},
57	}
58
59	for _, tt := range tests {
60		t.Run(tt.name, func(t *testing.T) {
61			blocked, err := IsBlockedTarget(tt.host)
62			require.NoError(t, err)
63			assert.Equal(t, tt.blocked, blocked, "IsBlockedTarget(%q) = %v, want %v", tt.host, blocked, tt.blocked)
64		})
65	}
66}
67
68func TestIsBlockedTarget_Hostnames(t *testing.T) {
69	tests := []struct {
70		name        string
71		host        string
72		wantBlocked bool
73		wantErr     bool
74	}{
75		// Localhost hostname - should block
76		{
77			name:        "localhost",
78			host:        "localhost",
79			wantBlocked: true,
80			wantErr:     false,
81		},
82
83		// Public domains - should allow (assuming they resolve to public IPs)
84		{
85			name:        "google.com",
86			host:        "google.com",
87			wantBlocked: false,
88			wantErr:     false,
89		},
90
91		// Non-existent domain - should error and block
92		{
93			name:        "non-existent domain",
94			host:        "this-domain-definitely-does-not-exist-12345.invalid",
95			wantBlocked: true,
96			wantErr:     true,
97		},
98	}
99
100	for _, tt := range tests {
101		t.Run(tt.name, func(t *testing.T) {
102			blocked, err := IsBlockedTarget(tt.host)
103
104			if tt.wantErr {
105				assert.Error(t, err)
106			}
107
108			assert.Equal(t, tt.wantBlocked, blocked, "IsBlockedTarget(%q) blocked = %v, want %v", tt.host, blocked, tt.wantBlocked)
109		})
110	}
111}
112
113func TestValidateExternalRouteTarget(t *testing.T) {
114	tests := []struct {
115		name    string
116		host    string
117		wantErr error
118	}{
119		// Blocked targets
120		{
121			name:    "loopback IP",
122			host:    "127.0.0.1",
123			wantErr: domain.ErrSSRFBlocked,
124		},
125		{
126			name:    "private network",
127			host:    "10.0.0.1",
128			wantErr: domain.ErrSSRFBlocked,
129		},
130		{
131			name:    "AWS metadata",
132			host:    "169.254.169.254",
133			wantErr: domain.ErrSSRFBlocked,
134		},
135		{
136			name:    "localhost",
137			host:    "localhost",
138			wantErr: domain.ErrSSRFBlocked,
139		},
140
141		// Allowed targets
142		{
143			name:    "public IP",
144			host:    "8.8.8.8",
145			wantErr: nil,
146		},
147	}
148
149	for _, tt := range tests {
150		t.Run(tt.name, func(t *testing.T) {
151			err := ValidateExternalRouteTarget(tt.host)
152
153			if tt.wantErr != nil {
154				require.Error(t, err)
155				assert.True(t, errors.Is(err, tt.wantErr), "expected error %v, got %v", tt.wantErr, err)
156			} else {
157				assert.NoError(t, err)
158			}
159		})
160	}
161}
162
163func TestResolveAndValidateHost_IPAddresses(t *testing.T) {
164	tests := []struct {
165		name    string
166		host    string
167		wantIP  string
168		wantErr bool
169	}{
170		{"public IP returned as-is", "203.0.113.1", "203.0.113.1", false},
171		{"blocked loopback IP", "127.0.0.1", "", true},
172		{"blocked private IP", "10.0.0.1", "", true},
173		{"blocked metadata IP", "169.254.169.254", "", true},
174	}
175	for _, tt := range tests {
176		t.Run(tt.name, func(t *testing.T) {
177			ip, err := ResolveAndValidateHost(tt.host)
178			if tt.wantErr {
179				assert.Error(t, err)
180				assert.True(t, errors.Is(err, domain.ErrSSRFBlocked))
181			} else {
182				assert.NoError(t, err)
183				assert.Equal(t, tt.wantIP, ip)
184			}
185		})
186	}
187}
188
189func TestResolveAndValidateHost_Hostname(t *testing.T) {
190	// localhost should resolve to a blocked IP
191	_, err := ResolveAndValidateHost("localhost")
192	assert.Error(t, err)
193	assert.True(t, errors.Is(err, domain.ErrSSRFBlocked))
194
195	// Non-existent domain should error
196	_, err = ResolveAndValidateHost("this-domain-definitely-does-not-exist-12345.invalid")
197	assert.Error(t, err)
198	assert.True(t, errors.Is(err, domain.ErrSSRFBlocked))
199}
200
201func TestIsBlockedIP(t *testing.T) {
202	tests := []struct {
203		ip      string
204		blocked bool
205	}{
206		// Should block
207		{"127.0.0.1", true},
208		{"10.0.0.1", true},
209		{"172.16.0.1", true},
210		{"192.168.1.1", true},
211		{"169.254.169.254", true},
212		{"0.0.0.0", true},
213
214		// Should allow
215		{"8.8.8.8", false},
216		{"1.1.1.1", false},
217		{"203.0.113.1", false},
218	}
219
220	for _, tt := range tests {
221		t.Run(tt.ip, func(t *testing.T) {
222			ip := net.ParseIP(tt.ip)
223			require.NotNil(t, ip, "failed to parse IP: %s", tt.ip)
224
225			blocked := isBlockedIP(ip)
226			assert.Equal(t, tt.blocked, blocked, "isBlockedIP(%s) = %v, want %v", tt.ip, blocked, tt.blocked)
227		})
228	}
229}

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