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, 04:45 UTC
requestbody: replace only known placeholders in the set body (#8008)
The set body is user-supplied and commonly JSON, but it was expanded with ReplaceAll, which blanks any {...} the replacer does not recognize, so a plain JSON body was sent empty and a body mixing JSON with a real placeholder was mangled Use ReplaceKnown, matching the respond body, so unrecognized braces are left intact and real placeholders still expand. Regression test pins both directions Co-authored-by: hktitof <hktitof@users.noreply.github.com>
Abdellatif Anaflous committed
Sep 26, 2026 at 09:50 UTC
81a0b6d838721c31283977c8f816a7fd1306051d
2 files changed
+156
-1
modules/caddyhttp/requestbody/requestbody.go
+4
-1
@@ -56,7 +56,10 @@ func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next cad
56
}
57
}
58
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
59
- replacedBody := repl.ReplaceAll(rb.Set, "")
59
+ // Only known placeholders are replaced: the body is user-supplied and
60
+ // often JSON, whose braces would otherwise be read as placeholders and
61
+ // blanked, sending an empty body instead. This matches the respond body.
62
+ replacedBody := repl.ReplaceKnown(rb.Set, "")
63
r.Body = io.NopCloser(strings.NewReader(replacedBody))
64
r.ContentLength = int64(len(replacedBody))
65
}
modules/caddyhttp/requestbody/requestbody_test.go
new
+152
@@ -0,0 +1,152 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package requestbody
16
+
17
+import (
18
+ "context"
19
+ "io"
20
+ "net/http"
21
+ "net/http/httptest"
22
+ "testing"
23
+
24
+ "github.com/caddyserver/caddy/v2"
25
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26
+)
27
+
28
+func newRequestBodySetRequest(t *testing.T) (*http.Request, *caddy.Replacer) {
29
+ t.Helper()
30
+
31
+ req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", nil)
32
+ repl := caddy.NewReplacer()
33
+ ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
34
+
35
+ return req.WithContext(ctx), repl
36
+}
37
+
38
+// TestRequestBodySetKeepsUnknownPlaceholders is a regression test for the JSON
39
+// case: a set body is user-supplied and commonly JSON, so braces the replacer
40
+// does not recognize must survive the expansion instead of being blanked, the
41
+// same known-placeholder policy the respond body already follows.
42
+func TestRequestBodySetKeepsUnknownPlaceholders(t *testing.T) {
43
+ const jsonBody = `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}`
44
+
45
+ rb := RequestBody{Set: jsonBody}
46
+
47
+ req, _ := newRequestBodySetRequest(t)
48
+
49
+ var gotBody string
50
+ next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
51
+ body, err := io.ReadAll(r.Body)
52
+ if err != nil {
53
+ return err
54
+ }
55
+ gotBody = string(body)
56
+ return nil
57
+ })
58
+
59
+ err := rb.ServeHTTP(httptest.NewRecorder(), req, next)
60
+ if err != nil {
61
+ t.Fatalf("did not expect an error, but got: %v", err)
62
+ }
63
+
64
+ if gotBody != jsonBody {
65
+ t.Errorf("set body was mangled by placeholder expansion: got %q, want %q", gotBody, jsonBody)
66
+ }
67
+}
68
+
69
+// TestRequestBodySetStillReplacesKnownPlaceholders guards the other direction:
70
+// keeping unknown braces intact must not stop real placeholders from being
71
+// replaced.
72
+func TestRequestBodySetStillReplacesKnownPlaceholders(t *testing.T) {
73
+ rb := RequestBody{Set: `{"token":"{caddy_request_body_test_token}"}`}
74
+ want := `{"token":"s3cret"}`
75
+
76
+ req, repl := newRequestBodySetRequest(t)
77
+ repl.Set("caddy_request_body_test_token", "s3cret")
78
+
79
+ var gotBody string
80
+ next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
81
+ body, err := io.ReadAll(r.Body)
82
+ if err != nil {
83
+ return err
84
+ }
85
+ gotBody = string(body)
86
+ return nil
87
+ })
88
+
89
+ err := rb.ServeHTTP(httptest.NewRecorder(), req, next)
90
+ if err != nil {
91
+ t.Fatalf("did not expect an error, but got: %v", err)
92
+ }
93
+
94
+ if gotBody != want {
95
+ t.Errorf("known placeholder not expanded: got %q, want %q", gotBody, want)
96
+ }
97
+}
98
+
99
+// TestRequestBodySetHeaderPlaceholders verifies that HTTP request header placeholders
100
+// resolve to an empty string when absent under ReplaceKnown, and expand to the header
101
+// value when present, so the backend never receives literal placeholder braces.
102
+func TestRequestBodySetHeaderPlaceholders(t *testing.T) {
103
+ for _, tc := range []struct {
104
+ name string
105
+ setBody string
106
+ setupReq func(req *http.Request)
107
+ wantBody string
108
+ }{
109
+ {
110
+ name: "absent header resolves to empty string",
111
+ setBody: `{"api_key":"{http.request.header.X-Api-Key}"}`,
112
+ wantBody: `{"api_key":""}`,
113
+ },
114
+ {
115
+ name: "present header resolves to value",
116
+ setBody: `{"api_key":"{http.request.header.X-Api-Key}"}`,
117
+ setupReq: func(req *http.Request) {
118
+ req.Header.Set("X-Api-Key", "secret-token")
119
+ },
120
+ wantBody: `{"api_key":"secret-token"}`,
121
+ },
122
+ } {
123
+ t.Run(tc.name, func(t *testing.T) {
124
+ rb := RequestBody{Set: tc.setBody}
125
+
126
+ req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", nil)
127
+ if tc.setupReq != nil {
128
+ tc.setupReq(req)
129
+ }
130
+ _ = caddyhttp.NewTestReplacer(req)
131
+
132
+ var gotBody string
133
+ next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
134
+ body, err := io.ReadAll(r.Body)
135
+ if err != nil {
136
+ return err
137
+ }
138
+ gotBody = string(body)
139
+ return nil
140
+ })
141
+
142
+ err := rb.ServeHTTP(httptest.NewRecorder(), req, next)
143
+ if err != nil {
144
+ t.Fatalf("did not expect an error, but got: %v", err)
145
+ }
146
+
147
+ if gotBody != tc.wantBody {
148
+ t.Errorf("got %q, want %q", gotBody, tc.wantBody)
149
+ }
150
+ })
151
+ }
152
+}