@marcel91200 / caddy Mirror · read-only / commits / 7e83775e
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, 14:45 UTC

Merge commit from fork

Only apply repl.ReplaceAll() on values from literal variable names (e.g. map outputs), not on values resolved from placeholder keys (e.g. {http.request.header.*}). The placeholder path already resolves the value via repl.Get(), so a second expansion allows user-controlled input containing {env.*} or {file.*} to be evaluated, leaking environment variables and file contents. Add regression test to verify placeholder-sourced values are not re-expanded.

Sam.An committed Mar 5, 2026 at 01:08 UTC 7e83775e3adea8b8da72fea3b159207bd71000dd
2 files changed +20 -2
modules/caddyhttp/matchers_test.go
+10 -1
@@ -967,6 +967,7 @@ func TestVarREMatcher(t *testing.T) {
967 desc string
968 match MatchVarsRE
969 input VarsMiddleware
970 + headers http.Header
971 expect bool
972 expectRepl map[string]string
973 }{
@@ -1001,6 +1002,14 @@ func TestVarREMatcher(t *testing.T) {
1002 input: VarsMiddleware{"Var1": "var1Value"},
1003 expect: true,
1004 },
1005 + {
1006 + desc: "placeholder key value containing braces is not double-expanded",
1007 + match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}},
1008 + input: VarsMiddleware{},
1009 + headers: http.Header{"X-Input": []string{"{env.HOME}"}},
1010 + expect: true,
1011 + expectRepl: map[string]string{"val.0": "{env.HOME}"},
1012 + },
1013 } {
1014 t.Run(tc.desc, func(t *testing.T) {
1015 t.Parallel()
@@ -1017,7 +1026,7 @@ func TestVarREMatcher(t *testing.T) {
1026 }
1027
1028 // set up the fake request and its Replacer
1020 - req := &http.Request{URL: new(url.URL), Method: http.MethodGet}
1029 + req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers}
1030 repl := caddy.NewReplacer()
1031 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1032 ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
modules/caddyhttp/vars.go
+10 -1
@@ -312,10 +312,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
312 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
313 for key, val := range m {
314 var varValue any
315 + var fromPlaceholder bool
316 if strings.HasPrefix(key, "{") &&
317 strings.HasSuffix(key, "}") &&
318 strings.Count(key, "{") == 1 {
319 varValue, _ = repl.Get(strings.Trim(key, "{}"))
320 + fromPlaceholder = true
321 } else {
322 varValue = vars[key]
323 }
@@ -334,7 +336,14 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
336 varStr = fmt.Sprintf("%v", vv)
337 }
338
337 - valExpanded := repl.ReplaceAll(varStr, "")
339 + // Only expand placeholders in values from literal variable names
340 + // (e.g. map outputs). Values resolved from placeholder keys are
341 + // already final and must not be re-expanded, as that would allow
342 + // user input like {env.SECRET} to be evaluated.
343 + valExpanded := varStr
344 + if !fromPlaceholder {
345 + valExpanded = repl.ReplaceAll(varStr, "")
346 + }
347 if match := val.Match(valExpanded, repl); match {
348 return match, nil
349 }