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