@marcel91200 / caddy Mirror · read-only / commits / 256df3c8
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

map: reject malformed destination placeholders (#8074)

Provision previously only checked for a single opening brace at the start of a destination, so values like {result}suffix, {result, and {result}} passed validation. strings.Trim(dest, "{}") then silently stored a wrong placeholder name (e.g. result}suffix), making the intended value unavailable. Require the destination to be exactly one placeholder: a single opening brace at the start, a single closing brace at the end, and a non-empty name. Fixes #8073

gelsomino committed Sep 28, 2026 at 20:19 UTC 256df3c8ad4ed93c58a7e83470a7f15ef8cb1b94
3 files changed +96 -3
caddytest/integration/map_test.go
+45
@@ -65,6 +65,51 @@ func TestMapRespondWithDefault(t *testing.T) {
65 tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
66 }
67
68 +func TestMapInvalidDestination(t *testing.T) {
69 + // see https://github.com/caddyserver/caddy/issues/8073
70 + failureCases := []struct {
71 + name string
72 + destination string
73 + expectedError string
74 + }{
75 + {
76 + name: "trailing text",
77 + destination: "{dest-name}suffix",
78 + expectedError: "destination 0 must be a placeholder and only a placeholder, but got '{dest-name}suffix'",
79 + },
80 + {
81 + name: "missing closing brace",
82 + destination: "{dest-name",
83 + expectedError: "destination 0 must be a placeholder and only a placeholder, but got '{dest-name'",
84 + },
85 + {
86 + name: "double closing brace",
87 + destination: "{dest-name}}",
88 + expectedError: "destination 0 must be a placeholder and only a placeholder, but got '{dest-name}}'",
89 + },
90 + }
91 +
92 + for _, tc := range failureCases {
93 + t.Run(tc.name, func(t *testing.T) {
94 + caddytest.AssertLoadError(t, `
95 + {
96 + skip_install_trust
97 + admin localhost:2999
98 + http_port 9080
99 + https_port 9443
100 + }
101 +
102 + localhost:9080 {
103 + map {http.request.uri.path} `+tc.destination+` {
104 + /foo mapped
105 + }
106 + respond "{result}"
107 + }
108 + `, "caddyfile", tc.expectedError)
109 + })
110 + }
111 +}
112 +
113 func TestMapAsJSON(t *testing.T) {
114 // arrange
115 tester := caddytest.NewTester(t)
modules/caddyhttp/map/map.go
+5 -3
@@ -64,10 +64,12 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
64 // Provision sets up h.
65 func (h *Handler) Provision(_ caddy.Context) error {
66 for j, dest := range h.Destinations {
67 - if strings.Count(dest, "{") != 1 || !strings.HasPrefix(dest, "{") {
68 - return fmt.Errorf("destination must be a placeholder and only a placeholder")
67 + if !strings.HasPrefix(dest, "{") || !strings.HasSuffix(dest, "}") ||
68 + strings.Count(dest, "{") != 1 || strings.Count(dest, "}") != 1 ||
69 + len(dest) < 3 {
70 + return fmt.Errorf("destination %d must be a placeholder and only a placeholder, but got '%s'", j, dest)
71 }
70 - h.Destinations[j] = strings.Trim(dest, "{}")
72 + h.Destinations[j] = dest[1 : len(dest)-1]
73 }
74
75 for i, m := range h.Mappings {
modules/caddyhttp/map/map_test.go
+46
@@ -43,6 +43,52 @@ func TestValidateDuplicateInputs(t *testing.T) {
43 }
44 }
45
46 +func TestProvisionDestinations(t *testing.T) {
47 + for _, tc := range []struct {
48 + name string
49 + destination string
50 + want string
51 + wantErr bool
52 + }{
53 + {name: "valid placeholder", destination: "{output}", want: "output"},
54 + {name: "trailing text", destination: "{output}suffix", wantErr: true},
55 + {name: "leading text", destination: "prefix{output}", wantErr: true},
56 + {name: "missing opening brace", destination: "output}", wantErr: true},
57 + {name: "missing closing brace", destination: "{output", wantErr: true},
58 + {name: "double closing brace", destination: "{output}}", wantErr: true},
59 + {name: "double opening brace", destination: "{{output}", wantErr: true},
60 + {name: "nested braces", destination: "{out{put}}", wantErr: true},
61 + {name: "empty braces", destination: "{}", wantErr: true},
62 + {name: "no braces", destination: "output", wantErr: true},
63 + } {
64 + t.Run(tc.name, func(t *testing.T) {
65 + h := Handler{
66 + Source: "{http.request.uri.path}",
67 + Destinations: []string{tc.destination},
68 + Mappings: []Mapping{
69 + {
70 + Input: "/foo",
71 + Outputs: []any{"FOO"},
72 + },
73 + },
74 + }
75 + err := h.Provision(caddy.Context{})
76 + if tc.wantErr {
77 + if err == nil {
78 + t.Fatalf("expected error for destination %q, but got none (stored %q)", tc.destination, h.Destinations[0])
79 + }
80 + return
81 + }
82 + if err != nil {
83 + t.Fatalf("unexpected error for destination %q: %v", tc.destination, err)
84 + }
85 + if h.Destinations[0] != tc.want {
86 + t.Fatalf("expected destination %q, got %q", tc.want, h.Destinations[0])
87 + }
88 + })
89 + }
90 +}
91 +
92 func TestHandler(t *testing.T) {
93 for i, tc := range []struct {
94 handler Handler