GHSA-x5w9-xh9r-mvfc Caddy: Remote Admin Authorization Bypass in `/config` API via Array Index Normalization
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 DisclosureThe 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.SummaryA remote admin client certificate restricted to the following path:can still read and modify a different array element by requesting:/config/apps/http/servers/srv/routes/01This happens because:the authorization layer uses string prefix matchingthe /config traversal layer parses array indices numerically using strconv.Atoi()So:authorization sees /.../01 as matching /.../0traversal resolves 01 to numeric index 1the 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 ScopeThis is a security bug in Caddy's own code:no browser behavior is involvedno dependency bug is involvedno external system compromise is involvedno third-party software compromise is requiredno unsafe content hosting or file upload is requiredThis is also not just “an unsafe configuration”.The configuration explicitly attempts to limit access to one specific path:/config/apps/http/servers/srv/routes/0But 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 CodeAuthorization path matching:admin.go:719Authorization config comment:admin.go:213Config traversal with numeric parsing:admin.go:1201admin.go:1310Root CauseAuthorization layerTraversal layeridx, err = strconv.Atoi(idxStr)and later:partInt, err := strconv.Atoi(part)Because of that:allowed path: /config/.../routes/0requested path: /config/.../routes/01authorization decision: allowedactual object selected: routes[1]Why This Is Not Just a “Prefix” CaseFor 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/handleThose 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 formtraversal uses the numeric formthe two refer to different objectsThis should be treated as an authorization bug rather than a documented prefix behavior.Security ImpactA remote admin identity restricted to one /config array element can:read a different array elementmodify a different array elementThis 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 ProductTested on:v2.11.2-3-gdf65455bAffected area:remote adminadmin.remote.access_control.permissions.paths/config API paths containing numeric array indicesThe reporter reproduced this on current HEAD.Minimal Reproduction ConfigurationCommands1. Generate client certificate2. Convert to base64 DER3. Start CaddySpecific Minimal Reproduction StepsStep 1: Read the explicitly authorized objectObserved result:Step 2: Read a different object using a leading-zero indexObserved result:This shows that a client limited to routes/0 can read routes[1].Step 3: Confirm that the traversal layer is interpreting the component numericallyObserved result: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 objectObserved result:Step 5: Confirm the unauthorized modificationObserved result:That confirms the client was able to modify routes[1], even though only /routes/0 was authorized.Precise Requests and Captured OutputAuthorized readUnauthorized readNumeric index interpretation evidenceUnauthorized modificationConfirmation of unauthorized modificationFull Log OutputRelevant startup logs from the reproduction run:Suggested FixThe 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 0allow 1reject 01reject 002One possible helper:Then use that helper anywhere /config array indices are parsed.Why This Fix Makes SenseThis 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/1but reject:/routes/01/routes/002That removes the authorization/resource mismatch.Suggested Regression TestsAllow /config/apps/http/servers/srv/routes/0, request /.../routes/0, expect allowed.Allow /config/apps/http/servers/srv/routes/0, request /.../routes/01, expect denied or invalid.Allow /config/apps/http/servers/srv/routes/0, request /.../routes/02, expect denied or invalid.With PATCH allowed on /.../routes/0, verify that /.../routes/01 cannot modify routes[1].
CVSS v3.1 score: 5.4