@marcel91200 / caddy Mirror · read-only / commits / 18ab0f95
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, 12:15 UTC

admin: reject non-canonical config array indices (#7592)

* admin: reject non-canonical config array indices * admin: expand canonical array index test coverage * Update admin.go Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * Update admin.go Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * admin: improve canonical array index test diagnostics --------- Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Amemoyoi committed May 1, 2026 at 00:39 UTC 18ab0f955fc1075d7727c7658dbfb734c673a5c9
2 files changed +62 -2
admin.go
+17 -2
@@ -1161,6 +1161,20 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
1161 return nil
1162 }
1163
1164 +func parseCanonicalArrayIndex(idx string) (int, error) {
1165 + if idx == "" {
1166 + return 0, fmt.Errorf("empty index")
1167 + }
1168 + i, err := strconv.Atoi(idx)
1169 + if err != nil {
1170 + return 0, err
1171 + }
1172 + if strconv.Itoa(i) != idx {
1173 + return 0, fmt.Errorf("non-canonical array index")
1174 + }
1175 + return i, nil
1176 +}
1177 +
1178 // unsyncedConfigAccess traverses into the current config and performs
1179 // the operation at path according to method, using body and out as
1180 // needed. This is a low-level, unsynchronized function; most callers
@@ -1222,11 +1236,12 @@ traverseLoop:
1236 var idx int
1237 if method != http.MethodPost {
1238 idxStr := parts[len(parts)-1]
1225 - idx, err = strconv.Atoi(idxStr)
1239 + idx, err = parseCanonicalArrayIndex(idxStr)
1240 if err != nil {
1241 return fmt.Errorf("[%s] invalid array index '%s': %v",
1242 path, idxStr, err)
1243 }
1244 +
1245 if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
1246 return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
1247 }
@@ -1326,7 +1341,7 @@ traverseLoop:
1341 }
1342
1343 case []any:
1329 - partInt, err := strconv.Atoi(part)
1344 + partInt, err := parseCanonicalArrayIndex(part)
1345 if err != nil {
1346 return fmt.Errorf("[/%s] invalid array index '%s': %v",
1347 strings.Join(parts[:i+1], "/"), part, err)
admin_test.go
+45
@@ -15,6 +15,7 @@
15 package caddy
16
17 import (
18 + "bytes"
19 "context"
20 "crypto"
21 "crypto/tls"
@@ -1106,3 +1107,47 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
1107 })
1108 }
1109 }
1110 +
1111 +func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
1112 + rawCfg = map[string]any{
1113 + rawConfigKey: map[string]any{
1114 + "list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
1115 + },
1116 + }
1117 +
1118 + tests := []struct {
1119 + name string
1120 + path string
1121 + wantOutput string
1122 + wantErr bool
1123 + }{
1124 + {name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
1125 + {name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
1126 + {name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
1127 + {name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
1128 + {name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
1129 + {name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
1130 + {name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
1131 + }
1132 +
1133 + for i, tc := range tests {
1134 + t.Run(tc.name, func(t *testing.T) {
1135 + var gotOutput bytes.Buffer
1136 + err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)
1137 +
1138 + if tc.wantErr {
1139 + if err == nil {
1140 + t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
1141 + }
1142 + return
1143 + }
1144 +
1145 + if err != nil {
1146 + t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
1147 + }
1148 + if gotOutput.String() != tc.wantOutput {
1149 + t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
1150 + }
1151 + })
1152 + }
1153 +}