@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 274 lines 9.04 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package integration
16
17 import (
18 "fmt"
19 "net/http"
20 "net/http/httptest"
21 "strings"
22 "sync"
23 "testing"
24
25 "github.com/stretchr/testify/assert"
26
27 "github.com/caddyserver/caddy/v2/caddytest"
28 )
29
30 // TestForwardAuthCopyHeadersStripsClientHeaders is a regression test for the
31 // header injection vulnerability in forward_auth copy_headers.
32 //
33 // When the auth service returns 200 OK without one of the copy_headers headers,
34 // the MatchNot guard skips the Set operation. Before this fix, the original
35 // client-supplied header survived unchanged into the backend request, allowing
36 // privilege escalation with only a valid (non-privileged) bearer token. After
37 // the fix, an unconditional delete route runs first, so the backend always
38 // sees an absent header rather than the attacker-supplied value.
39 func TestForwardAuthCopyHeadersStripsClientHeaders(t *testing.T) {
40 // Mock auth service: accepts any Bearer token, returns 200 OK with NO
41 // identity headers. This is the stateless JWT validator pattern that
42 // triggers the vulnerability.
43 authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44 if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
45 w.WriteHeader(http.StatusOK)
46 return
47 }
48 w.WriteHeader(http.StatusUnauthorized)
49 }))
50 defer authSrv.Close()
51
52 // Mock backend: records the identity headers it receives. A real application
53 // would use X-User-Id / X-User-Role to make authorization decisions.
54 type received struct{ userID, userRole string }
55 var (
56 mu sync.Mutex
57 last received
58 )
59 backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60 mu.Lock()
61 last = received{
62 userID: r.Header.Get("X-User-Id"),
63 userRole: r.Header.Get("X-User-Role"),
64 }
65 mu.Unlock()
66 w.WriteHeader(http.StatusOK)
67 fmt.Fprint(w, "ok")
68 }))
69 defer backendSrv.Close()
70
71 authAddr := strings.TrimPrefix(authSrv.URL, "http://")
72 backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
73
74 tester := caddytest.NewTester(t)
75 tester.InitServer(fmt.Sprintf(`
76 {
77 skip_install_trust
78 admin localhost:2999
79 http_port 9080
80 https_port 9443
81 grace_period 1ns
82 }
83 http://localhost:9080 {
84 forward_auth %s {
85 uri /
86 copy_headers X-User-Id X-User-Role
87 }
88 reverse_proxy %s
89 }
90 `, authAddr, backendAddr), "caddyfile")
91
92 // Case 1: no token. Auth must still reject the request even when the client
93 // includes identity headers. This confirms the auth check is not bypassed.
94 req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
95 req.Header.Set("X-User-Id", "injected")
96 req.Header.Set("X-User-Role", "injected")
97 resp := tester.AssertResponseCode(req, http.StatusUnauthorized)
98 resp.Body.Close()
99
100 // Case 2: valid token, no injected headers. The backend should see absent
101 // identity headers (the auth service never returns them).
102 req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
103 req.Header.Set("Authorization", "Bearer token123")
104 tester.AssertResponse(req, http.StatusOK, "ok")
105 mu.Lock()
106 gotID, gotRole := last.userID, last.userRole
107 mu.Unlock()
108 if gotID != "" {
109 t.Errorf("baseline: X-User-Id should be absent, got %q", gotID)
110 }
111 if gotRole != "" {
112 t.Errorf("baseline: X-User-Role should be absent, got %q", gotRole)
113 }
114
115 // Case 3 (the security regression): valid token plus forged identity headers.
116 // The fix must strip those values so the backend never sees them.
117 req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
118 req.Header.Set("Authorization", "Bearer token123")
119 req.Header.Set("X-User-Id", "admin") // forged
120 req.Header.Set("X-User-Role", "superadmin") // forged
121 tester.AssertResponse(req, http.StatusOK, "ok")
122 mu.Lock()
123 gotID, gotRole = last.userID, last.userRole
124 mu.Unlock()
125 if gotID != "" {
126 t.Errorf("injection: X-User-Id must be stripped, got %q", gotID)
127 }
128 if gotRole != "" {
129 t.Errorf("injection: X-User-Role must be stripped, got %q", gotRole)
130 }
131 }
132
133 // TestForwardAuthCopyHeadersAuthResponseWins verifies that when the auth
134 // service does include a copy_headers header in its response, that value
135 // is forwarded to the backend and takes precedence over any client-supplied
136 // value for the same header.
137 func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
138 const wantUserID = "service-user-42"
139 const wantUserRole = "editor"
140
141 // Auth service: accepts bearer token and sets identity headers.
142 authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143 if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
144 w.Header().Set("X-User-Id", wantUserID)
145 w.Header().Set("X-User-Role", wantUserRole)
146 w.WriteHeader(http.StatusOK)
147 return
148 }
149 w.WriteHeader(http.StatusUnauthorized)
150 }))
151 defer authSrv.Close()
152
153 type received struct{ userID, userRole string }
154 var (
155 mu sync.Mutex
156 last received
157 )
158 backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
159 mu.Lock()
160 last = received{
161 userID: r.Header.Get("X-User-Id"),
162 userRole: r.Header.Get("X-User-Role"),
163 }
164 mu.Unlock()
165 w.WriteHeader(http.StatusOK)
166 fmt.Fprint(w, "ok")
167 }))
168 defer backendSrv.Close()
169
170 authAddr := strings.TrimPrefix(authSrv.URL, "http://")
171 backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
172
173 tester := caddytest.NewTester(t)
174 tester.InitServer(fmt.Sprintf(`
175 {
176 skip_install_trust
177 admin localhost:2999
178 http_port 9080
179 https_port 9443
180 grace_period 1ns
181 }
182 http://localhost:9080 {
183 forward_auth %s {
184 uri /
185 copy_headers X-User-Id X-User-Role
186 }
187 reverse_proxy %s
188 }
189 `, authAddr, backendAddr), "caddyfile")
190
191 // The client sends forged headers; the auth service overrides them with
192 // its own values. The backend must receive the auth service values.
193 req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
194 req.Header.Set("Authorization", "Bearer token123")
195 req.Header.Set("X-User-Id", "forged-id") // must be overwritten
196 req.Header.Set("X-User-Role", "forged-role") // must be overwritten
197 tester.AssertResponse(req, http.StatusOK, "ok")
198
199 mu.Lock()
200 gotID, gotRole := last.userID, last.userRole
201 mu.Unlock()
202 if gotID != wantUserID {
203 t.Errorf("X-User-Id: want %q, got %q", wantUserID, gotID)
204 }
205 if gotRole != wantUserRole {
206 t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole)
207 }
208 }
209
210 // TestForwardAuthCopyHeadersUnderscoreAlias guards GHSA-f59h-q822-g45g and
211 // GHSA-49wc-4hcv-v58q: client-supplied `Remote_user`/`Remote.user` aliases
212 // of the copy_headers target `Remote-User` must be stripped before the
213 // auth route runs, otherwise a downstream CGI/FastCGI/PHP backend would
214 // fold all three names into the same HTTP_REMOTE_USER variable and the
215 // attacker would override the trusted identity.
216 func TestForwardAuthCopyHeadersUnderscoreAlias(t *testing.T) {
217 const wantRemoteUser = "alice"
218
219 authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
220 w.Header().Set("Remote-User", wantRemoteUser)
221 w.WriteHeader(http.StatusOK)
222 }))
223 t.Cleanup(authSrv.Close)
224
225 type received struct {
226 remoteUserHyphen, remoteUserUnderscore, remoteUserDot string
227 }
228 var (
229 mu sync.Mutex
230 last received
231 )
232 backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
233 mu.Lock()
234 last = received{
235 remoteUserHyphen: r.Header.Get("Remote-User"),
236 remoteUserUnderscore: strings.Join(r.Header["Remote_user"], ","),
237 remoteUserDot: strings.Join(r.Header["Remote.user"], ","),
238 }
239 mu.Unlock()
240 fmt.Fprint(w, "ok")
241 }))
242 t.Cleanup(backendSrv.Close)
243
244 tester := caddytest.NewTester(t)
245 tester.InitServer(fmt.Sprintf(`
246 {
247 skip_install_trust
248 admin localhost:2999
249 http_port 9080
250 https_port 9443
251 grace_period 1ns
252 }
253 http://localhost:9080 {
254 forward_auth %s {
255 uri /
256 copy_headers Remote-User
257 }
258 reverse_proxy %s
259 }
260 `, strings.TrimPrefix(authSrv.URL, "http://"), strings.TrimPrefix(backendSrv.URL, "http://")), "caddyfile")
261
262 req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
263 // Set the underscore and dot aliases via raw map access to bypass
264 // http.Header canonicalization, as an attacker would on the wire.
265 req.Header["Remote_user"] = []string{"attacker"}
266 req.Header["Remote.user"] = []string{"attacker"}
267 tester.AssertResponse(req, http.StatusOK, "ok")
268
269 mu.Lock()
270 defer mu.Unlock()
271 assert.Equal(t, wantRemoteUser, last.remoteUserHyphen, "trusted Remote-User must reach the backend")
272 assert.Empty(t, last.remoteUserUnderscore, "underscore alias must be dropped")
273 assert.Empty(t, last.remoteUserDot, "dot alias must be dropped")
274 }