mirror of
https://github.com/caddyserver/caddy.git
synced 2026-08-04 06:53:36 +00:00
rewrite: prevent placeholder re-expansion in injected query (#7761)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Tests / test (s390x on IBM Z) (push) Has been cancelled
Tests / goreleaser-check (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, aix) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, linux) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, windows) (push) Has been cancelled
Lint / lint (push) Has been cancelled
Lint / lint-1 (push) Has been cancelled
Lint / lint-2 (push) Has been cancelled
Lint / govulncheck (push) Has been cancelled
Lint / dependency-review (push) Has been cancelled
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Tests / test (s390x on IBM Z) (push) Has been cancelled
Tests / goreleaser-check (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, aix) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, linux) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Has been cancelled
Cross-Build / build (~1.26.0, 1.26, windows) (push) Has been cancelled
Lint / lint (push) Has been cancelled
Lint / lint-1 (push) Has been cancelled
Lint / lint-2 (push) Has been cancelled
Lint / govulncheck (push) Has been cancelled
Lint / dependency-review (push) Has been cancelled
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
When the rewrite URI template ends with a literal '?' and contains a placeholder that expands to client-controlled bytes (e.g. {http.request.header.X-Fwd}), those bytes flow into buildQueryString which runs a second Replacer pass. If the bytes contain placeholder syntax such as {env.SECRET}, that placeholder is evaluated, allowing disclosure of environment variables, files (via {file./path}), or internal request vars through the rewritten request URI.
Escape '{' and '}' in the injected query before assigning it to the query variable, so the second pass cannot find any placeholder syntax to evaluate. Operator-written placeholders in the rewrite template are already expanded by the first pass on the path component, so the only '{' or '}' surviving into the injected query must have come from replacement values.
Fixes GHSA-j8px-rmrx-76h9.
Includes three regression tests mirroring the 'is not re-expanded' tests in modules/caddyhttp/vars_test.go.
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This commit is contained in:
parent
4c04143261
commit
176b043b01
2 changed files with 45 additions and 0 deletions
|
|
@ -223,6 +223,15 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
|||
newPath, injectedQuery = before, after
|
||||
// don't overwrite explicitly-configured query string
|
||||
if query == "" {
|
||||
// the injected query came from the first-pass placeholder
|
||||
// expansion above, which means any '{' or '}' bytes in it
|
||||
// must have come from replacement values (e.g. a request
|
||||
// header), not from operator-written placeholder syntax.
|
||||
// escape them so buildQueryString does not re-expand them,
|
||||
// which would allow attacker input like {env.SECRET} to be
|
||||
// evaluated (see GHSA-j8px-rmrx-76h9).
|
||||
injectedQuery = strings.ReplaceAll(injectedQuery, "{", "%7B")
|
||||
injectedQuery = strings.ReplaceAll(injectedQuery, "}", "%7D")
|
||||
query = injectedQuery
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
|
|
@ -350,6 +351,32 @@ func TestRewrite(t *testing.T) {
|
|||
input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),
|
||||
expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"),
|
||||
},
|
||||
|
||||
// regression tests for GHSA-j8px-rmrx-76h9: when the rewrite URI
|
||||
// ends with a literal '?', the first-pass placeholder expansion
|
||||
// may produce a path containing attacker-controlled bytes that
|
||||
// then get split at '?' and fed into buildQueryString, which runs
|
||||
// a SECOND placeholder pass. Bytes injected via a header value (or
|
||||
// any other client-controlled placeholder) must not be treated as
|
||||
// placeholder syntax during this second pass.
|
||||
{
|
||||
// literal header value containing placeholder syntax is not re-expanded into query
|
||||
rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
|
||||
input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "foo?{env.CADDY_REWRITE_TEST_SECRET}=leak"),
|
||||
expect: newRequest(t, "GET", "/serve/foo?%7Benv.CADDY_REWRITE_TEST_SECRET%7D=leak"),
|
||||
},
|
||||
{
|
||||
// literal header value with placeholder syntax in query position is not re-expanded
|
||||
rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
|
||||
input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?key={env.CADDY_REWRITE_TEST_SECRET}"),
|
||||
expect: newRequest(t, "GET", "/serve/ok?key=%7Benv.CADDY_REWRITE_TEST_SECRET%7D"),
|
||||
},
|
||||
{
|
||||
// literal header value with embedded file placeholder is not re-expanded
|
||||
rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
|
||||
input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?path={file./etc/passwd}"),
|
||||
expect: newRequest(t, "GET", "/serve/ok?path=%7Bfile./etc/passwd%7D"),
|
||||
},
|
||||
} {
|
||||
// copy the original input just enough so that we can
|
||||
// compare it after the rewrite to see if it changed
|
||||
|
|
@ -364,6 +391,9 @@ func TestRewrite(t *testing.T) {
|
|||
repl.Set("http.request.uri", tc.input.RequestURI)
|
||||
repl.Set("http.request.uri.path", tc.input.URL.Path)
|
||||
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
|
||||
for field, vals := range tc.input.Header {
|
||||
repl.Set("http.request.header."+field, strings.Join(vals, ","))
|
||||
}
|
||||
|
||||
// we can't directly call Provision() without a valid caddy.Context
|
||||
// (TODO: fix that) so here we ad-hoc compile the regex
|
||||
|
|
@ -456,6 +486,12 @@ func newRequest(t *testing.T, method, uri string) *http.Request {
|
|||
return req
|
||||
}
|
||||
|
||||
func newRequestWithHeader(t *testing.T, method, uri, headerKey, headerVal string) *http.Request {
|
||||
req := newRequest(t, method, uri)
|
||||
req.Header.Set(headerKey, headerVal)
|
||||
return req
|
||||
}
|
||||
|
||||
// reqEqual if r1 and r2 are equal enough for our purposes.
|
||||
func reqEqual(r1, r2 *http.Request) bool {
|
||||
if r1.Method != r2.Method {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue