Feb 25, 2026·5 min read·8 visits
Caddy versions prior to 2.11.1 contain a critical logic flaw in how they handle path matching with percent-encoded sequences. By capitalizing parts of the URL, an attacker can bypass path-based restrictions, potentially accessing administrative panels or protected files that rely on Caddy for security.
A logic error in Caddy's HTTP path matching engine allows attackers to bypass access control lists (ACLs) by manipulating URL casing. When a configuration pattern involves percent-encoded characters, Caddy fails to normalize the incoming request path to lowercase before comparison. This oversight renders case-insensitive rules ineffective, effectively letting attackers walk past the bouncer simply by shouting their destination (e.g., requesting '/ADMIN' instead of '/admin').
Caddy is the darling of the modern web server ecosystem. It's written in Go, it handles TLS automatically, and it markets itself as 'secure by default.' We love it because it usually stops us from shooting ourselves in the foot. But even the sleekest, most memory-safe tools can trip over the oldest hurdle in computer science: string comparison.
In the world of reverse proxies, the path matcher is often the primary line of defense. You set up a rule: "Block everything starting with /admin." You assume the server is smart enough to know that /ADMIN, /Admin, and /aDmin are the same place. Usually, Caddy is that smart.
However, CVE-2026-27587 reveals a crack in that armor. It turns out that if you get fancy with your configuration—specifically by including percent-encoded characters (like %2f for a slash)—Caddy's brain short-circuits. It stops normalizing the input. It stops thinking. And suddenly, your carefully crafted access control list is about as effective as a 'Keep Out' sign written in invisible ink.
To understand this bug, you have to understand how Caddy optimizes matching. String comparison is expensive at scale, so Caddy tries to normalize everything upfront. When you provision the server, Caddy takes your configuration patterns (like /secret/*) and lowercases them. This way, when a request comes in, Caddy just lowercases the request and does a fast check.
But here's the catch: generic lowercasing destroys percent-encoding information. If you configured a rule for /api%2fv1 (implying a literal encoded slash), a standard lowercase operation might mangle the semantic meaning Caddy is trying to preserve.
So, the developers added a fork in the road. If the configuration pattern contains a % character, Caddy switches to a specialized function: matchPatternWithEscapeSequence. This function is supposed to handle the nuance of encoded characters.
The problem? In this specialized 'smart' branch, they forgot the basics. While the configuration pattern was already lowercased during the provisioning phase, the incoming request path was left raw. Caddy ends up comparing a lowercased rule against a raw, potentially mixed-case request. It's an apples-to-oranges comparison where the attacker controls the oranges.
Let's look at the diff. This is from modules/caddyhttp/matchers.go. It is almost painfully simple, which is characteristic of the most dangerous logic bugs.
The vulnerable function, matchPatternWithEscapeSequence, takes the escapedPath (from the request) and the matchPath (from the config). Remember, matchPath is already lowercase here.
Before the fix:
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
// ... logic to handle wildcards ...
return strings.HasPrefix(escapedPath, matchPath)
}See the issue? escapedPath comes straight from the wire. If I send /ADMIN%2f, escapedPath is /ADMIN%2f. But matchPath is /admin%2f. "/ADMIN%2f" does not start with "/admin%2f" in a case-sensitive byte comparison.
The Fix (Commit a108119):
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
+ escapedPath = strings.ToLower(escapedPath)
// We would just compare the pattern against r.URL.Path,
// but the pattern contains %, indicating that we should
// compare at least some part of the path in raw/escaped
// form. Matches are case-insensitive.
// ...
}It is a one-line fix. By forcefully lowercasing the request path at the start of the function, parity is restored. The attacker's /ADMIN%2f becomes /admin%2f, the match succeeds, and the ACL (hopefully) blocks the request.
Exploiting this requires two things: a Caddy server running a version older than 2.11.1, and a specific configuration anti-pattern.
First, we need a target configuration that looks something like this. Note the %2f in the path—this is the trigger that forces Caddy into the vulnerable code path:
{
"match": [{"path": ["/private%2fdata*"]}],
"handle": [{"handler": "authentication"}]
}In this scenario, the admin wants to protect anything under /private/data. Because they used %2f, Caddy treats this as a 'complex' match.
The Attack Chain:
GET /private%2fdata/passwords.txt. Caddy sees the match, triggers the authentication handler, and we get a 401 Unauthorized or a login redirect.GET /PRIVATE%2fdata/passwords.txt.% in the config pattern.matchPatternWithEscapeSequence./private%2fdata* vs request /PRIVATE%2fdata....authentication handler is skipped. The request falls through to the next handler—usually a file server or a reverse proxy to a backend app.If the backend app (e.g., a Python or Node.js service) handles routing case-insensitively (which most do) or normalizes the path itself, it will happily serve the content. We have successfully bypassed the auth layer.
The severity of this vulnerability depends entirely on how Caddy is being used. If Caddy is just a dumb pipe passing everything to a backend that does its own strict authentication, this is a non-issue.
However, the modern trend is 'Sidecar Security.' DevOps teams love to offload authentication (OIDC, Basic Auth, mTLS) to the edge proxy (Caddy). They assume that if Caddy says 'you shall not pass,' then nobody passes.
This vulnerability creates a normalization inconsistency. The security layer (Caddy) thinks the path is one thing (/PRIVATE... -> No Match -> Allowed), while the application layer thinks it is another (/PRIVATE... -> Normalize -> /private... -> Serve Resource). This mismatch allows unauthenticated users to access administrative panels, metrics endpoints, or sensitive file directories simply by holding down the Shift key.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
Caddy Caddy | < 2.11.1 | 2.11.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-178 |
| CVSS Score | 7.7 (High) |
| Attack Vector | Network |
| Vulnerability Type | Improper Handling of Case Sensitivity |
| Exploit Status | PoC Available (Trivial) |
| Patch Date | 2026-02-20 |