From 7e83775e3adea8b8da72fea3b159207bd71000dd Mon Sep 17 00:00:00 2001 From: "Sam.An" <56215891+sammiee5311@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:08:39 +0900 Subject: [PATCH] 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. --- modules/caddyhttp/matchers_test.go | 11 ++++++++++- modules/caddyhttp/vars.go | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 160aa424f..c3d8c405e 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -967,6 +967,7 @@ func TestVarREMatcher(t *testing.T) { desc string match MatchVarsRE input VarsMiddleware + headers http.Header expect bool expectRepl map[string]string }{ @@ -1001,6 +1002,14 @@ func TestVarREMatcher(t *testing.T) { input: VarsMiddleware{"Var1": "var1Value"}, expect: true, }, + { + desc: "placeholder key value containing braces is not double-expanded", + match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}}, + input: VarsMiddleware{}, + headers: http.Header{"X-Input": []string{"{env.HOME}"}}, + expect: true, + expectRepl: map[string]string{"val.0": "{env.HOME}"}, + }, } { t.Run(tc.desc, func(t *testing.T) { t.Parallel() @@ -1017,7 +1026,7 @@ func TestVarREMatcher(t *testing.T) { } // set up the fake request and its Replacer - req := &http.Request{URL: new(url.URL), Method: http.MethodGet} + req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers} repl := caddy.NewReplacer() ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index d01f4a431..f19ca16fc 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -312,10 +312,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for key, val := range m { var varValue any + var fromPlaceholder bool if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") && strings.Count(key, "{") == 1 { varValue, _ = repl.Get(strings.Trim(key, "{}")) + fromPlaceholder = true } else { varValue = vars[key] } @@ -334,7 +336,14 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { varStr = fmt.Sprintf("%v", vv) } - valExpanded := repl.ReplaceAll(varStr, "") + // Only expand placeholders in values from literal variable names + // (e.g. map outputs). Values resolved from placeholder keys are + // already final and must not be re-expanded, as that would allow + // user input like {env.SECRET} to be evaluated. + valExpanded := varStr + if !fromPlaceholder { + valExpanded = repl.ReplaceAll(varStr, "") + } if match := val.Match(valExpanded, repl); match { return match, nil }