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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 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 | + | } | |
| @@ -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 | + | } | |||