fix(deps): update module github.com/caddyserver/caddy/v2 to v2.11.2 [security]#401
Open
renovate-sh-app[bot] wants to merge 1 commit intomainfrom
Open
Conversation
Author
|
…security] | datasource | package | from | to | | ---------- | ------------------------------- | ------ | ------- | | go | github.com/caddyserver/caddy/v2 | v2.9.1 | v2.11.2 | Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
9cbd8f6 to
7aa39a0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
v2.9.1→v2.11.2Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
GitHub Vulnerability Alerts
CVE-2026-27589
commit: e0f8d9b2047af417d8faf354b675941f3dac9891 (as-of 2026-02-04)
channel: GitHub security advisory (per SECURITY.md)
summary
The local caddy admin API (default listen
127.0.0.1:2019) exposes a state-changingPOST /loadendpoint that replaces the entire running configuration.When origin enforcement is not enabled (
enforce_originnot configured), the admin endpoint accepts cross-origin requests (e.g., from attacker-controlled web content in a victim browser) and applies an attacker-supplied JSON config. this can change the admin listener settings and alter HTTP server behavior without user intent.Severity
Medium
Justification:
Affected component
caddyconfig/load.go: adminLoad.handleLoad(/loadadmin endpoint)Reproduction
Attachment:
poc.zip(integration harness) with canonical and control runs.Expected output (excerpt):
Control output (excerpt):
Impact
An attacker can replace the running caddy configuration via the local admin API. Depending on the deployed configuration/modules, this can:
Suggested remediation
Ensure cross-origin web content cannot trigger
POST /loadon the local admin API by default, for example by:/load(and other state-changing admin endpoints).poc.zip
PR_DESCRIPTION.md
CVE-2026-27585
Summary
The path sanitization in file matcher doesn't sanitize backslashes which can lead to bypassing path related security protections.
Details
The try_files directive is used to rewrite the request uri. It accepts a list of patterns and checks if any files exist in the root that match the provided patterns. It's commonly used in Caddy configs. For example, it's used in SPA applications to rewrite every route that doesn't exist as a file to
index.html.try_filespatterns are actually glob patterns and file matcher expands them. The{path}in the pattern is replaced withthe request path and then is expanded by
fs.Glob. The request path is sanitized before being placed inside the pattern and the special chars are escaped . The following code is the sanitization part.The problem here is that it does not escape backslashes.
/something-\*/can match a file namedsomething-\-anything.txt, but it should not. The primitive that this vulnerability provides is not very useful, as it only allows an attacker to guess filenames that contain a backslash and they should also know the characters before that backslash.The backslash is mainly used to escape special characters in glob patterns, but when it appears before non special characters, it is ignored. This means that
h\ello*matcheshello worldeven thougheis not a special character. This behavior can be abused to bypass path protections that might be in place. For example, if there is a reverse proxy that only allows/documents/*to the internal network and its upstream is a Caddy server that usestry_files, the reverse proxy's protection can be bypassed by requesting the path/do%5ccuments/.Some configurations that implement blacklisting and serving together in Caddy are also vulnerable but there's a condition that the
try_filesdirective and the filteringroute/handlemust not be in a same block becausetry_filesdirective executes beforerouteandhandledirectives.For example the following config isn't vulnerable.
But this one is vulnerable.
This config is also vulnerable because
Headerdirectives executes beforetry_files.PoC
Paste this script somewhere and run it. It should print "some content" which means that the nginx protection has failed.
Impact
This vulnerability may allow an attacker to bypass security protections. It affects users with specific Caddy and environment configurations.
AI Usage
An LLM was used to polish this report.
CVE-2026-27586
Summary
Two swallowed errors in
ClientAuthentication.provision()cause mTLS client certificate authentication to silently fail open when a CA certificate file is missing, unreadable, or malformed. The server starts without error but accepts any client certificate signed by any system-trusted CA, completely bypassing the intended private CA trust boundary.Details
In
modules/caddytls/connpolicy.go, theprovision()method has tworeturn nilstatements that should bereturn err:Bug #1 — line 787:
Bug #2 — line 800:
Compare with line 811 which correctly returns the error:
When the error is swallowed on line 787, the chain is:
TrustedCACertsremains empty (no DER data appended from the file)len(clientauth.TrustedCACerts) > 0guard on line 794 is false — skippedclientauth.CARawis nil — line 806 returns nilclientauth.caremains nil — no CA pool was createdprovision()returns nil — caller thinks provisioning succeededThen in
ConfigureTLSConfig():Active()returns true becauseTrustedCACertPEMFilesis non-emptyRequireAndVerifyClientCert(line 860)clientauth.cais nil, socfg.ClientCAsis never set (line 867 skipped)crypto/tlswithRequireAndVerifyClientCert+ nilClientCAsverifies client certs against the system root pool instead of the intended CAThe fix is changing
return niltoreturn erron lines 787 and 800.PoC
Start Caddy — it starts without any error or warning.
Connect with any client certificate (even self-signed):
A full Go test that proves the bug end-to-end (including a successful TLS handshake with a random self-signed client cert) is here: https://gist.github.com/moscowchill/9566c79c76c0b64c57f8bd0716f97c48
Test output:
Impact
Any deployment using
trusted_ca_cert_fileortrusted_ca_certs_pem_filesfor mTLS will silently degrade to accepting any system-trusted client certificate if the CA file becomes unavailable. This can happen due to a typo in the path, file rotation, corruption, or permission changes. The server gives no indication that mTLS is misconfigured.CVE-2026-27587
Summary
Caddy's HTTP
pathrequest matcher is intended to be case-insensitive, but when the match pattern contains percent-escape sequences (%xx) it compares against the request's escaped path without lowercasing. An attacker can bypass path-based routing and any access controls attached to that route by changing the casing of the request path.Details
In Caddy
v2.10.2,MatchPathis explicitly designed to be case-insensitive and lowercases match patterns during provisioning:modules/caddyhttp/matchers.go: rationale captured in theMatchPathcomment.MatchPath.Provisionlowercases configured patterns viastrings.ToLower.MatchPath.MatchWithErrorlowercases the request path for the normal matching path:reqPath := strings.ToLower(r.URL.Path).But when a match pattern contains a percent sign (
%),MatchPath.MatchWithErrorswitches to "escaped space" matching and builds the comparison string fromr.URL.EscapedPath():reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)continues (skipping the remaining matching logic for that pattern).Because
r.URL.EscapedPath()is not lowercased, case differences in the request path can cause the escaped-space match to fail even thoughMatchPathis meant to be case-insensitive. For example, with a pattern of/admin%2Fpanel:/admin%2Fpanelmatches and can be denied as intended./ADMIN%2Fpaneldoes not match and falls through to other routes/handlers.Suggested fix
%-pattern matching path, ensure the effective string passed topath.Matchis lowercased (same as the normal branch).matchPatternWithEscapeSequenceright beforepath.Match.Reproduced on:
v2.10.2-- this is the release referenced in the reproduction below.v2.11.0-beta.2.58968b3fd38cacbf4b5e07cc8c8be27696dce60f.PoC
Prereqs:
/opt/caddy-2.10.2/caddy(editCADDY_BINin the script if needed)Script (Click to expand)
Expected output (Click to expand)
== Caddy version == v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8= == Caddyfile == { debug } :8080 { log @​block { path /admin%2Fpanel } respond @​block "DENY" 403 respond "ALLOW" 200 } == Start Caddy (debug + capture logs) == cmd: /opt/caddy-2.10.2/caddy run --config /tmp/tmp.GXiRbxOnBN/Caddyfile --adapter caddyfile == Request 1 (baseline - expect deny) == cmd: curl -v -H 'Host: example.test' http://127.0.0.1:8080/admin%2Fpanel * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 * using HTTP/1.x > GET /admin%2Fpanel HTTP/1.1 > Host: example.test > User-Agent: curl/8.15.0 > Accept: */* > * Request completely sent off < HTTP/1.1 403 Forbidden < Content-Type: text/plain; charset=utf-8 < Server: Caddy < Date: Sun, 08 Feb 2026 22:19:20 GMT < Content-Length: 4 < * Connection #​0 to host 127.0.0.1 left intact DENY == Request 2 (BYPASS - expect allow) == cmd: curl -v -H 'Host: example.test' http://127.0.0.1:8080/ADMIN%2Fpanel * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 * using HTTP/1.x > GET /ADMIN%2Fpanel HTTP/1.1 > Host: example.test > User-Agent: curl/8.15.0 > Accept: */* > * Request completely sent off < HTTP/1.1 200 OK < Content-Type: text/plain; charset=utf-8 < Server: Caddy < Date: Sun, 08 Feb 2026 22:19:20 GMT < Content-Length: 5 < * Connection #​0 to host 127.0.0.1 left intact ALLOW == Stop Caddy == == Full Caddy debug log == {"level":"info","ts":1770589158.3687892,"msg":"maxprocs: Leaving GOMAXPROCS=4: CPU quota undefined"} {"level":"info","ts":1770589158.3690693,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":1844136345,"previous":9223372036854775807} {"level":"info","ts":1770589158.369109,"msg":"using config from file","file":"/tmp/tmp.GXiRbxOnBN/Caddyfile"} {"level":"info","ts":1770589158.3704133,"msg":"adapted config to JSON","adapter":"caddyfile"} {"level":"warn","ts":1770589158.370424,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"/tmp/tmp.GXiRbxOnBN/Caddyfile","line":2} {"level":"info","ts":1770589158.3715324,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]} {"level":"debug","ts":1770589158.3716462,"logger":"http.auto_https","msg":"adjusted config","tls":{"automation":{"policies":[{}]}},"http":{"servers":{"srv0":{"listen":[":8080"],"routes":[{"handle":[{"body":"DENY","handler":"static_response","status_code":403}]},{"handle":[{"body":"ALLOW","handler":"static_response","status_code":200}]}],"automatic_https":{},"logs":{}}}}} {"level":"debug","ts":1770589158.3718414,"logger":"http","msg":"starting server loop","address":"[::]:8080","tls":false,"http3":false} {"level":"warn","ts":1770589158.371858,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":8080"} {"level":"warn","ts":1770589158.3718607,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":8080"} {"level":"info","ts":1770589158.3718636,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]} {"level":"debug","ts":1770589158.3718896,"logger":"events","msg":"event","name":"started","id":"6bb8b6fe-4980-4a48-9f7e-2146ecd48ce6","origin":"","data":null} {"level":"info","ts":1770589158.3720388,"msg":"autosaved config (load with --resume flag)","file":"/home/vh/.config/caddy/autosave.json"} {"level":"info","ts":1770589158.3720443,"msg":"serving initial configuration"} {"level":"info","ts":1770589158.372355,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00064d180"} {"level":"info","ts":1770589158.3855736,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/home/vh/.local/share/caddy","instance":"a259f82d-3c7c-4706-9ca8-17456b4af729","try_again":1770675558.3855705,"try_again_in":86399.999999388} {"level":"info","ts":1770589158.3857276,"logger":"tls","msg":"finished cleaning storage units"} {"level":"info","ts":1770589160.2764065,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57126","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"example.test","uri":"/admin%2Fpanel","headers":{"User-Agent":["curl/8.15.0"],"Accept":["*/*"]}},"bytes_read":0,"user_id":"","duration":0.000017493,"size":4,"status":403,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/plain; charset=utf-8"]}} {"level":"info","ts":1770589160.2943857,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57136","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"example.test","uri":"/ADMIN%2Fpanel","headers":{"User-Agent":["curl/8.15.0"],"Accept":["*/*"]}},"bytes_read":0,"user_id":"","duration":0.000066734,"size":5,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/plain; charset=utf-8"]}} {"level":"info","ts":1770589160.2966497,"msg":"shutting down apps, then terminating","signal":"SIGTERM"} {"level":"warn","ts":1770589160.2966666,"msg":"exiting; byeee!! 👋","signal":"SIGTERM"} {"level":"debug","ts":1770589160.296728,"logger":"events","msg":"event","name":"stopping","id":"aefb0a2f-0a81-4587-9f79-e530883c3fe1","origin":"","data":null} {"level":"info","ts":1770589160.2967443,"logger":"http","msg":"servers shutting down with eternal grace period"} {"level":"info","ts":1770589160.2968848,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"} {"level":"info","ts":1770589160.2968912,"msg":"shutdown complete","signal":"SIGTERM","exit_code":0}Impact
This is a route/auth bypass in Caddy's path-matching logic for patterns that include escape sequences. Deployments that use
pathmatchers with%xxpatterns to block or protect sensitive endpoints (including encoded-path variants such as encoded slashes) can be bypassed by changing the casing of the request path, allowing unauthorized access to sensitive endpoints behind Caddy depending on upstream configuration.The reproduction is minimal per the reporting guidance. In a realistic "full" scenario, a deployment may block
%xxvariants likepath /admin%2Fpanel, otherwise proxying. If the backend is case-insensitive/normalizing,/ADMIN%2Fpanelmaps to the same handler; Caddy’s%-pattern match misses due to case, so the block is skipped and the request falls through.AI Use Disclosure
A custom AI agent pipeline was used to discover the vulnerability, after which was manually reproduced and validated each step. The entire report was ran through an LLM to make sure nothing obvious was missed.
Disclosure/crediting
Asim Viladi Oglu Manizada
CVE-2026-27588
Summary
Caddy's HTTP
hostrequest matcher is documented as case-insensitive, but when configured with a large host list (>100 entries) it becomes case-sensitive due to an optimized matching path. An attacker can bypass host-based routing and any access controls attached to that route by changing the casing of theHostheader.Details
In Caddy
v2.10.2, theMatchHostmatcher states it matches the Host value case-insensitively:modules/caddyhttp/matchers.go:type MatchHost matches requests by the Host value (case-insensitive).However, in
MatchHost.MatchWithError, when the host list is considered "large" (len(m) > 100):MatchHost.large()returns true forlen(m) > 100(modules/caddyhttp/matchers.go, around thelarge()helper).m[pos] == reqHost).strings.EqualFold(reqHost, host)check from ever running.Net effect: with a host list length of 101 or more, changing only the casing of the incoming
Hostheader can cause thehostmatcher to not match when it should.Suggested fix
MatchHost.Provision(at least for non-fuzzy entries).reqHost) to lower-case before the large-list binary search + equality check, so the optimized path stays case-insensitive.Reproduced on:
v2.10.2-- this is the release I reference in the repro below.v2.11.0-beta.2.58968b3fd38cacbf4b5e07cc8c8be27696dce60f.PoC
Prereqs:
/opt/caddy-2.10.2/caddy(editCADDY_BINin the script if needed)Script (Click to expand)
Expected output (Click to expand)
== Caddy version == v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8= == Caddyfile == { debug } :8080 { log @​protected { host h001.test h002.test h003.test h004.test h005.test h006.test h007.test h008.test h009.test h010.test h011.test h012.test h013.test h014.test h015.test h016.test h017.test h018.test h019.test h020.test h021.test h022.test h023.test h024.test h025.test h026.test h027.test h028.test h029.test h030.test h031.test h032.test h033.test h034.test h035.test h036.test h037.test h038.test h039.test h040.test h041.test h042.test h043.test h044.test h045.test h046.test h047.test h048.test h049.test h050.test h051.test h052.test h053.test h054.test h055.test h056.test h057.test h058.test h059.test h060.test h061.test h062.test h063.test h064.test h065.test h066.test h067.test h068.test h069.test h070.test h071.test h072.test h073.test h074.test h075.test h076.test h077.test h078.test h079.test h080.test h081.test h082.test h083.test h084.test h085.test h086.test h087.test h088.test h089.test h090.test h091.test h092.test h093.test h094.test h095.test h096.test h097.test h098.test h099.test h100.test h101.test path /admin } respond @​protected "DENY" 403 respond "ALLOW" 200 } == Start Caddy (debug + capture logs) == cmd: /opt/caddy-2.10.2/caddy run --config /tmp/tmp.3BN6rgj9yF/Caddyfile --adapter caddyfile == Request 1 (baseline - expect deny) == cmd: curl -v -H 'Host: h050.test' http://127.0.0.1:8080/admin * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 * using HTTP/1.x > GET /admin HTTP/1.1 > Host: h050.test > User-Agent: curl/8.15.0 > Accept: */* > * Request completely sent off < HTTP/1.1 403 Forbidden < Content-Type: text/plain; charset=utf-8 < Server: Caddy < Date: Sun, 08 Feb 2026 22:09:09 GMT < Content-Length: 4 < * Connection #​0 to host 127.0.0.1 left intact DENY == Request 2 (BYPASS - expect allow) == cmd: curl -v -H 'Host: H050.TEST' http://127.0.0.1:8080/admin * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 * using HTTP/1.x > GET /admin HTTP/1.1 > Host: H050.TEST > User-Agent: curl/8.15.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain; charset=utf-8 < Server: Caddy < Date: Sun, 08 Feb 2026 22:09:09 GMT < Content-Length: 5 < * Connection #​0 to host 127.0.0.1 left intact ALLOW == Stop Caddy == == Full Caddy debug log == {"level":"info","ts":1770588548.012352,"msg":"maxprocs: Leaving GOMAXPROCS=4: CPU quota undefined"} {"level":"info","ts":1770588548.0125406,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":1844136345,"previous":9223372036854775807} {"level":"info","ts":1770588548.0125597,"msg":"using config from file","file":"/tmp/tmp.3BN6rgj9yF/Caddyfile"} {"level":"info","ts":1770588548.0131946,"msg":"adapted config to JSON","adapter":"caddyfile"} {"level":"warn","ts":1770588548.013202,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"/tmp/tmp.3BN6rgj9yF/Caddyfile","line":2} {"level":"info","ts":1770588548.0139973,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//127.0.0.1:2019","//localhost:2019","//[::1]:2019"]} {"level":"debug","ts":1770588548.0140707,"logger":"http.auto_https","msg":"adjusted config","tls":{"automation":{"policies":[{}]}},"http":{"servers":{"srv0":{"listen":[":8080"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"body":"DENY","handler":"static_response","status_code":403}],"match":[{"host":["h001.test","h002.test","h003.test","h004.test","h005.test","h006.test","h007.test","h008.test","h009.test","h010.test","h011.test","h012.test","h013.test","h014.test","h015.test","h016.test","h017.test","h018.test","h019.test","h020.test","h021.test","h022.test","h023.test","h024.test","h025.test","h026.test","h027.test","h028.test","h029.test","h030.test","h031.test","h032.test","h033.test","h034.test","h035.test","h036.test","h037.test","h038.test","h039.test","h040.test","h041.test","h042.test","h043.test","h044.test","h045.test","h046.test","h047.test","h048.test","h049.test","h050.test","h051.test","h052.test","h053.test","h054.test","h055.test","h056.test","h057.test","h058.test","h059.test","h060.test","h061.test","h062.test","h063.test","h064.test","h065.test","h066.test","h067.test","h068.test","h069.test","h070.test","h071.test","h072.test","h073.test","h074.test","h075.test","h076.test","h077.test","h078.test","h079.test","h080.test","h081.test","h082.test","h083.test","h084.test","h085.test","h086.test","h087.test","h088.test","h089.test","h090.test","h091.test","h092.test","h093.test","h094.test","h095.test","h096.test","h097.test","h098.test","h099.test","h100.test","h101.test"],"path":["/admin"]}]},{"handle":[{"body":"ALLOW","handler":"static_response","status_code":200}]}]}],"terminal":true}],"automatic_https":{},"logs":{}}}}} {"level":"info","ts":1770588548.0143135,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0000d7c80"} {"level":"debug","ts":1770588548.0143793,"logger":"http","msg":"starting server loop","address":"[::]:8080","tls":false,"http3":false} {"level":"warn","ts":1770588548.014415,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":8080"} {"level":"warn","ts":1770588548.0144184,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":8080"} {"level":"info","ts":1770588548.0144203,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]} {"level":"debug","ts":1770588548.014438,"logger":"events","msg":"event","name":"started","id":"1c7f6534-d264-456d-988d-e9f77a099c42","origin":"","data":null} {"level":"info","ts":1770588548.0145273,"msg":"autosaved config (load with --resume flag)","file":"/home/vh/.config/caddy/autosave.json"} {"level":"info","ts":1770588548.0145316,"msg":"serving initial configuration"} {"level":"info","ts":1770588548.0274432,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/home/vh/.local/share/caddy","instance":"a259f82d-3c7c-4706-9ca8-17456b4af729","try_again":1770674948.0274422,"try_again_in":86399.999999709} {"level":"info","ts":1770588548.0275078,"logger":"tls","msg":"finished cleaning storage units"} {"level":"info","ts":1770588549.9694445,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"53220","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"h050.test","uri":"/admin","headers":{"User-Agent":["curl/8.15.0"],"Accept":["*/*"]}},"bytes_read":0,"user_id":"","duration":0.000014857,"size":4,"status":403,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/plain; charset=utf-8"]}} {"level":"info","ts":1770588549.9741833,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"53234","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"H050.TEST","uri":"/admin","headers":{"Accept":["*/*"],"User-Agent":["curl/8.15.0"]}},"bytes_read":0,"user_id":"","duration":0.00000551,"size":5,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/plain; charset=utf-8"]}} {"level":"info","ts":1770588549.9751372,"msg":"shutting down apps, then terminating","signal":"SIGTERM"} {"level":"warn","ts":1770588549.9751456,"msg":"exiting; byeee!! 👋","signal":"SIGTERM"} {"level":"debug","ts":1770588549.9751775,"logger":"events","msg":"event","name":"stopping","id":"e02c5e64-9d76-48b6-a967-4f003850bdd4","origin":"","data":null} {"level":"info","ts":1770588549.9751873,"logger":"http","msg":"servers shutting down with eternal grace period"} {"level":"info","ts":1770588549.975331,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"} {"level":"info","ts":1770588549.9753368,"msg":"shutdown complete","signal":"SIGTERM","exit_code":0}Impact
This is a route/auth bypass in Caddy's request-matching layer. Any internet-exposed Caddy deployment that relies on
hostmatchers with large host lists (>100) to select protected routes (e.g. applyingbasicauth,forward_auth,responddeny rules, or protectingreverse_proxybackends) can be bypassed by varying the case of theHostheader, allowing unauthorized access to sensitive endpoints depending on upstream configuration.The reproduction is minimal per the reporting guidance; a realistic "full" scenario is Caddy fronting a multi-tenant app and doing
forward_auth/basicauth/denyfor/adminonly when host is in a big (>100) allowlist, but the default handler stillreverse_proxying to the same app. Then sendingHost: H050.TESTskips the guarded route in Caddy, yet the upstream still treats it as the same tenant host -->/adminis reachable without the intended guard.AI Use Disclosure
A custom AI agent pipeline was used to discover the vulnerability, after which was manually reproduced and validated each step. The entire report was ran through an LLM for editing.
Disclosure/crediting
Asim Viladi Oglu Manizada
CVE-2026-27590
Summary
Caddy's FastCGI path splitting logic computes the split index on a lowercased copy of the request path and then uses that byte index to slice the original path. This is unsafe for Unicode because
strings.ToLower()can change UTF-8 byte length for some characters. As a result, Caddy can derive an incorrectSCRIPT_NAME/SCRIPT_FILENAMEandPATH_INFO, potentially causing a request that contains.phpto execute a different on-disk file than intended (path confusion). In setups where an attacker can control file contents (e.g., upload features), this can lead to unintended PHP execution of non-.php files (potential RCE depending on deployment).Details
The issue is in
github.com/caddyserver/caddy/modules/caddyhttp/fastcgi.Trasnport.splitPos()(and the subsequent slicing inbuildEnv()):The returned index is computed in the byte space of lowerPath, but
buildEnv()applies it to the original path:docURI = path[:splitPos]pathInfo = path[splitPos:]scriptName = strings.TrimSuffix(path, fc.pathInfo)scriptFilename = caddyhttp.SanitizedPathJoin(fc.documentRoot, fc.scriptName)This assumes
lowerPathandpathhave identical byte lengths and identical byte offsets, which is not true for some Unicode case mappings. Certain characters expand when lowercased (UTF-8 byte length increases), shifting the computed index. This creates a mismatch where.phpis found in the lowercased string at an offset that does not correspond to the same position in the original string, causing the split point to land later/earlier than intended.PoC
Create a small Go program that reproduces Caddy's
splitPos()behavior (compute the.phpsplit point on a lowercased path, then use that byte index on the original path):poc.go:go run poc.goOutput on my side:
Expected split is right after the first
.php(/ȺȺȺȺshell.php). Instead, the computed split lands later and cuts the original path aftershell.php.txt, leaving.phpas the remainder.Impact
Security boundary bypass/path confusion in script resolution.
In typical deployments,
.phpextension boundaries are relied on to decide what is executed by PHP. This bug can cause Caddy/FPM to execute a different file than intended by confusingSCRIPT_NAME/SCRIPT_FILENAME. If an attacker can place attacker-controlled content into a file that can be resolved asSCRIPT_FILENAME(common in web apps with uploads or writable directories), this can lead to unintended PHP execution of non-.php files and potentially remote code execution. Severity depends on deployment and presence of attacker-controlled file writes, but the primitive itself is remotely triggerable via crafted URLs.This vulnerability was initially reported to FrankenPHP (GHSA-g966-83w7-6w38) by @AbdrrahimDahmani. The affected code has been copied/adapted from Caddy, which, according to research, is also affected.
The patch is a port of the FrankenPHP patch.
Caddy: Improper sanitization of glob characters in file matcher may lead to bypassing security protections
CVE-2026-27585 / GHSA-4xrr-hq4w-6vf4 / GO-2026-4535
More information
Details
Summary
The path sanitization in file matcher doesn't sanitize backslashes which can lead to bypassing path related security protections.
Details
The try_files directive is used to rewrite the request uri. It accepts a list of patterns and checks if any files exist in the root that match the provided patterns. It's commonly used in Caddy configs. For example, it's used in SPA applications to rewrite every route that doesn't exist as a file to
index.html.try_filespatterns are actually glob patterns and file matcher expands them. The{path}in the pattern is replaced withthe request path and then is expanded by
fs.Glob. The request path is sanitized before being placed inside the pattern and the special chars are escaped . The following code is the sanitization part.The problem here is that it does not escape backslashes.
/something-\*/can match a file namedsomething-\-anything.txt, but it should not. The primitive that this vulnerability provides is not very useful, as it only allows an attacker to guess filenames that contain a backslash and they should also know the characters before that backslash.The backslash is mainly used to escape special characters in glob patterns, but when it appears before non special characters, it is ignored. This means that
h\ello*matcheshello worldeven thougheis not a special character. This behavior can be abused to bypass path protections that might be in place. For example, if there is a reverse proxy that only allows/documents/*to the internal network and its upstream is a Caddy server that usestry_files, the reverse proxy's protection can be bypassed by requesting the path/do%5ccuments/.Some configurations that implement blacklisting and serving together in Caddy are also vulnerable but there's a condition that the
try_filesdirective and the filteringroute/handlemust not be in a same block becausetry_filesdirective executes beforerouteandhandledirectives.For example the following config isn't vulnerable.
But this one is vulnerable.
This config is also vulnerable because
Headerdirectives executes beforetry_files.PoC
Paste this script somewhere and run it. It should print "some content" which means that the nginx protection has failed.
Impact
This vulnerability may allow an attacker to bypass security protections. It affects users with specific Caddy and environment configurations.
AI Usage
An LLM was used to polish this report.
Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:PReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Caddy: Unicode case-folding length expansion causes incorrect split_path index in FastCGI transport
CVE-2026-27590 / GHSA-5r3v-vc8m-m96g / GO-2026-4536
More information
Details
Summary
Caddy's FastCGI path splitting logic computes the split index on a lowercased copy of the request path and then uses that byte index to slice the original path. This is unsafe for Unicode because
strings.ToLower()can change UTF-8 byte length for some characters. As a result, Caddy can derive an incorrectSCRIPT_NAME/SCRIPT_FILENAMEandPATH_INFO, potentially causing a request that contains.phpto execute a different on-disk file than intended (path confusion). In setups where an attacker can control file contents (e.g., upload features), this can lead to unintended PHP execution of non-.php files (potential RCE depending on deployment).Details
The issue is in
github.com/caddyserver/caddy/modules/caddyhttp/fastcgi.Trasnport.splitPos()(and the subsequent slicing inbuildEnv()):The returned index is computed in the byte space of lowerPath, but
buildEnv()applies it to the original path:docURI = path[:splitPos]pathInfo = path[splitPos:]scriptName = strings.TrimSuffix(path, fc.pathInfo)scriptFilename = caddyhttp.SanitizedPathJoin(fc.documentRoot, fc.scriptName)This assumes
lowerPathandpathhave identical byte lengths and identical byte offsets, which is not true for some Unicode case mappings. Certain characters expand when lowercased (UTF-8 byte length increases), shifting the computed index. This creates a mismatch where.phpis found in the lowercased string at an offset that does not correspond to the same position in the original string, causing the split point to land later/earlier than intended.PoC
Create a small Go program that reproduces Caddy's
splitPos()behavior (compute the.phpsplit point on a lowercased path, then use that byte index on the original path):poc.go:go run poc.goOutput on my side:
Expected split is right after the first
.php(/ȺȺȺȺshell.php). Instead, the computed split lands later and cuts the original path aftershell.php.txt, leaving.phpas the remainder.Impact
Security boundary bypass/path confusion in script resolution.
In typical deployments,
.phpextension boundaries are relied on to decide what is executed by PHP. This bug can cause Caddy/FPM to execute a different file than intended by confusingSCRIPT_NAME/SCRIPT_FILENAME. If an attacker can place attacker-controlled content into a file that can be resolved asSCRIPT_FILENAME(common in web apps with uploads or writable directories), this can lead to unintended PHP execution of non-.php files and potentially remote code execution. Severity depends on deployment and presence of attacker-controlled file writes, but the primitive itself is remotely triggerable via crafted URLs.This vulnerability was initially reported to FrankenPHP (GHSA-g966-83w7-6w38) by @AbdrrahimDahmani. The affected code has been copied/adapted from Caddy, which, according to research, is also affected.
The patch is a port of the FrankenPHP patch.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:PReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Caddy is vulnerable to cross-origin config application via local admin API /load
CVE-2026-27589 / GHSA-879p-475x-rqh2 / GO-2026-4537
More information
Details
commit: e0f8d9b2047af417d8faf354b675941f3dac9891 (as-of 2026-02-04)
channel: GitHub security advisory (per SECURITY.md)
summary
The local caddy admin API (default listen
127.0.0.1:2019) exposes a state-changingPOST /loadendpoint that replaces the entire running configuration.When origin enforcement is not enabled (
enforce_originnot configured), the admin endpoint accepts cross-origin requests (e.g., from attacker-controlled web content in a victim browser) and applies an attacker-supplied JSON config. this can change the admin listener settings and alter HTTP server behavior without user intent.Severity
Medium
Justification:
Affected component
caddyconfig/load.go: adminLoad.handleLoad(/loadadmin endpoint)Reproduction
Attachment:
poc.zip(integration harness) with canonical and control runs.Expected output (excerpt):
Control output (excerpt):
Impact
An attacker can replace the running caddy configuration via the local admin API. Depending on the deployed configuration/modules, this can:
Suggested remediation
Ensure cross-origin web content cannot trigger
POST /loadon the local admin API by default, for example by:/load(and other state-changing admin endpoints).poc.zip
PR_DESCRIPTION.md
Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:PReferences