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
}