Skip to content

fix: don't compress the Mercure SSE endpoint in the sample Caddyfile - #2692

Open
dunglas wants to merge 1 commit into
mainfrom
fix/caddyfile-sse-no-encode
Open

dunglas wants to merge 1 commit into
mainfrom
fix/caddyfile-sse-no-encode

Conversation

@dunglas

@dunglas dunglas commented Oct 5, 2026

Copy link
Copy Markdown
Member

Since Caddy 2.11.7 (caddyserver/caddy#7905), encode compresses text/event-stream responses. Each subscriber then holds an encoder for the whole connection (a few MiB with zstd), and compressing tokens or private updates next to attacker-influenced data enables BREACH-style attacks.

This excludes the hub path from encode in the sample Caddyfile and documents the pattern in the Mercure docs. php-server --mercure isn't affected: its Mercure route runs before the encode route.

Same change as dunglas/mercure#1418.

Caddy 2.11.7 compresses text/event-stream responses. Each compressed
stream keeps an encoder for the whole connection, which costs memory per
subscriber, and compressing secrets alongside attacker-influenced data
enables BREACH-style attacks.
@nickchomey

Copy link
Copy Markdown

Can I suggest giving some more information - metrics etc... - about what the actual cost is of compressing the SSE stream? I ask this because it is very much my intention to compress the stream, as it can result in immense bandwidth savings - it is a standard approach for SSE-based web frameworks like Datastar

https://data-star.dev/guide/the_tao_of_datastar/#compression

@dunglas

dunglas commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Here are the numbers. Since Caddy 2.11.7, each compressed SSE connection keeps its encoder for the whole lifetime of the stream. With Caddy's default settings, that's about 2.7 MiB per subscriber with zstd and about 1 MiB with gzip. 10,000 concurrent subscribers then need around 27 GiB of RAM for zstd alone.

That persistent context is also what gives the high ratios mentioned in the Datastar guide: each event is compressed against the previous ones. It's also what makes BREACH more practical than on regular responses. Say a stream carries both a secret (for example a CSRF token in a rendered fragment) and content an attacker can influence (a chat message, a comment). An attacker who can observe the encrypted traffic can then publish guesses and read the size of each event, without making the victim send any request.

So it depends on the app, and the sample Caddyfile should default to the safe and cheap option. Compressing the stream stays a one-line change: encode zstd br gzip without the matcher. I can expand the docs to explain when it's worth enabling: large, repetitive payloads, no secrets mixed with user-controlled content, and enough memory for one encoder per connection.

@nickchomey

Copy link
Copy Markdown

Thanks for the information! Would definitely be useful to add such details to the docs.

How much ram does brotli require, in comparison?

By the way, the Sec-Fetch-Site header is a simple and viable replacement for CSRF tokens. Might mitigate some of that risk?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants