detached c5f57f6

test: add ProxyCIDRAllowlist coverage for redirect listener bypass

Exercises the proxy CIDR middleware wrapping a redirect handler to
ensure blocked IPs get 403 and XFF cannot bypass RemoteAddr checks.
commit
c5f57f6de8291f6b1b0fbe44a31f6a1c80a9af0b
author
brice · · 2026-03-28 05:43 UTC
parent
5a1e21d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +68 -0
M internal/adapters/in/http/middleware/cidr_test.go +68 -0
@@ -109,3 +109,71 @@ func TestRegistryCIDRAllowlist(t *testing.T) {
109109 })
110110 }
111111 }
112+
113+func TestProxyCIDRAllowlist(t *testing.T) {
114+ log := testLogger()
115+ redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116+ http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect)
117+ })
118+
119+ tests := []struct {
120+ name string
121+ allowedNets []*net.IPNet
122+ remoteAddr string
123+ wantStatus int
124+ }{
125+ {
126+ name: "no CIDRs configured passes through",
127+ allowedNets: nil,
128+ remoteAddr: "203.0.113.50:1234",
129+ wantStatus: http.StatusPermanentRedirect,
130+ },
131+ {
132+ name: "allowed IP gets redirect",
133+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
134+ remoteAddr: "173.245.48.1:1234",
135+ wantStatus: http.StatusPermanentRedirect,
136+ },
137+ {
138+ name: "blocked IP gets 403",
139+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
140+ remoteAddr: "203.0.113.50:1234",
141+ wantStatus: http.StatusForbidden,
142+ },
143+ {
144+ name: "localhost always allowed",
145+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
146+ remoteAddr: "127.0.0.1:1234",
147+ wantStatus: http.StatusPermanentRedirect,
148+ },
149+ {
150+ name: "ignores XFF (uses RemoteAddr only)",
151+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
152+ remoteAddr: "203.0.113.50:1234",
153+ wantStatus: http.StatusForbidden,
154+ },
155+ }
156+
157+ for _, tt := range tests {
158+ t.Run(tt.name, func(t *testing.T) {
159+ handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect)
160+
161+ req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
162+ req.RemoteAddr = tt.remoteAddr
163+ // Even with XFF from an "allowed" range, the proxy middleware
164+ // must check RemoteAddr, not forwarded headers.
165+ req.Header.Set("X-Forwarded-For", "173.245.48.1")
166+ rr := httptest.NewRecorder()
167+
168+ handler.ServeHTTP(rr, req)
169+
170+ assert.Equal(t, tt.wantStatus, rr.Code)
171+
172+ if tt.wantStatus == http.StatusForbidden {
173+ var errResp dto.ErrorResponse
174+ require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
175+ assert.Equal(t, "Forbidden", errResp.Error)
176+ }
177+ })
178+ }
179+}
@@ -109,3 +109,71 @@ func TestRegistryCIDRAllowlist(t *testing.T) {
109 })109 })
110 }110 }
111 }111 }
112+
113+func TestProxyCIDRAllowlist(t *testing.T) {
114+ log := testLogger()
115+ redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116+ http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect)
117+ })
118+
119+ tests := []struct {
120+ name string
121+ allowedNets []*net.IPNet
122+ remoteAddr string
123+ wantStatus int
124+ }{
125+ {
126+ name: "no CIDRs configured passes through",
127+ allowedNets: nil,
128+ remoteAddr: "203.0.113.50:1234",
129+ wantStatus: http.StatusPermanentRedirect,
130+ },
131+ {
132+ name: "allowed IP gets redirect",
133+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
134+ remoteAddr: "173.245.48.1:1234",
135+ wantStatus: http.StatusPermanentRedirect,
136+ },
137+ {
138+ name: "blocked IP gets 403",
139+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
140+ remoteAddr: "203.0.113.50:1234",
141+ wantStatus: http.StatusForbidden,
142+ },
143+ {
144+ name: "localhost always allowed",
145+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
146+ remoteAddr: "127.0.0.1:1234",
147+ wantStatus: http.StatusPermanentRedirect,
148+ },
149+ {
150+ name: "ignores XFF (uses RemoteAddr only)",
151+ allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
152+ remoteAddr: "203.0.113.50:1234",
153+ wantStatus: http.StatusForbidden,
154+ },
155+ }
156+
157+ for _, tt := range tests {
158+ t.Run(tt.name, func(t *testing.T) {
159+ handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect)
160+
161+ req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
162+ req.RemoteAddr = tt.remoteAddr
163+ // Even with XFF from an "allowed" range, the proxy middleware
164+ // must check RemoteAddr, not forwarded headers.
165+ req.Header.Set("X-Forwarded-For", "173.245.48.1")
166+ rr := httptest.NewRecorder()
167+
168+ handler.ServeHTTP(rr, req)
169+
170+ assert.Equal(t, tt.wantStatus, rr.Code)
171+
172+ if tt.wantStatus == http.StatusForbidden {
173+ var errResp dto.ErrorResponse
174+ require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
175+ assert.Equal(t, "Forbidden", errResp.Error)
176+ }
177+ })
178+ }
179+}

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 8

Tags 106

↑↓ moveenter openesc close