18
"net/http"
19
"reflect"
20
"regexp"
21
+ "strings"
22
"testing"
23
24
"github.com/caddyserver/caddy/v2"
351
input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),
352
expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"),
353
},
354
+
355
+ // regression tests for GHSA-j8px-rmrx-76h9: when the rewrite URI
356
+ // ends with a literal '?', the first-pass placeholder expansion
357
+ // may produce a path containing attacker-controlled bytes that
358
+ // then get split at '?' and fed into buildQueryString, which runs
359
+ // a SECOND placeholder pass. Bytes injected via a header value (or
360
+ // any other client-controlled placeholder) must not be treated as
361
+ // placeholder syntax during this second pass.
362
+ {
363
+ // literal header value containing placeholder syntax is not re-expanded into query
364
+ rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
365
+ input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "foo?{env.CADDY_REWRITE_TEST_SECRET}=leak"),
366
+ expect: newRequest(t, "GET", "/serve/foo?%7Benv.CADDY_REWRITE_TEST_SECRET%7D=leak"),
367
+ },
368
+ {
369
+ // literal header value with placeholder syntax in query position is not re-expanded
370
+ rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
371
+ input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?key={env.CADDY_REWRITE_TEST_SECRET}"),
372
+ expect: newRequest(t, "GET", "/serve/ok?key=%7Benv.CADDY_REWRITE_TEST_SECRET%7D"),
373
+ },
374
+ {
375
+ // literal header value with embedded file placeholder is not re-expanded
376
+ rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
377
+ input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?path={file./etc/passwd}"),
378
+ expect: newRequest(t, "GET", "/serve/ok?path=%7Bfile./etc/passwd%7D"),
379
+ },
380
} {
381
// copy the original input just enough so that we can
382
// compare it after the rewrite to see if it changed
391
repl.Set("http.request.uri", tc.input.RequestURI)
392
repl.Set("http.request.uri.path", tc.input.URL.Path)
393
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
394
+ for field, vals := range tc.input.Header {
395
+ repl.Set("http.request.header."+field, strings.Join(vals, ","))
396
+ }
397
398
// we can't directly call Provision() without a valid caddy.Context
399
// (TODO: fix that) so here we ad-hoc compile the regex
486
return req
487
}
488
489
+func newRequestWithHeader(t *testing.T, method, uri, headerKey, headerVal string) *http.Request {
490
+ req := newRequest(t, method, uri)
491
+ req.Header.Set(headerKey, headerVal)
492
+ return req
493
+}
494
+
495
// reqEqual if r1 and r2 are equal enough for our purposes.
496
func reqEqual(r1, r2 *http.Request) bool {
497
if r1.Method != r2.Method {