@marcel91200 / caddy Mirror · read-only / commits / 81a0b6d8
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, 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 +}