Skip to content

mod_cache: Check err_headers_out too for a Vary: * response - #909

Merged
covener merged 1 commit into
apache:2.4.xfrom
arturobernalg:fix/httpd-70289-cache-vary-star-err-headers
Nov 1, 2026
Merged

covener merged 1 commit into
apache:2.4.xfrom
arturobernalg:fix/httpd-70289-cache-vary-star-err-headers

Conversation

@arturobernalg

@arturobernalg arturobernalg commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

mod_cache's cacheability check for Vary: * only looked at r->headers_out. When Vary: * is set with Header always set Vary "*" instead of Header set, it lands in r->err_headers_out instead, so the check missed it - but store_headers() does merge err_headers_out in (via ap_cache_cacheable_headers_out() / cache_merge_headers_out()), so the response got stored anyway, and could later be served to a different client from the cache.

On top of that, cache_select() had a related read-side bug: when matching a stored response's Vary header against the current request, a * token was looked up as a literal request header named *. Since no such header ever exists, both sides came back NULL and the code treated that as a match instead of RFC 9111 §4.1's "a stored response with Vary: * always fails to match" rule. So a Vary: * entry already on disk - written before this fix, or during a rolling upgrade across mixed versions - could still be served to the wrong client even with the store-side fix alone.

Impact: reproduced end to end against a real httpd with a disk cache in front of a proxied backend. A backend that varies its body by a cookie while marking the response Cache-Control: public, max-age=600 (already questionable backend behaviour, but not something httpd can see), behind Header always set Vary "*": a request with Cookie: session=victim gets cached, and a following request with no cookie gets X-Cache: HIT and the victim's body back. This is a cross-client information disclosure through the shared cache, gated behind that specific admin configuration (Header always + a backend that marks per-identity content as publicly cacheable). Separately reproduced the cache_select() gap with a two-binary test: a pre-fix build wrote a Vary: * entry to disk, then a post-fix build serving from the same CacheRoot still served it to a mismatched request, until cache_select() was fixed too.

Fix: check err_headers_out too in cache_save_filter(), matching how this same function already treats Expires / Last-Modified / Etag / Cache-Control / Pragma a few lines above (err_headers_out first, falling back to headers_out). And in cache_select(), treat a stored Vary: * token as an automatic, unconditional mismatch rather than an absent-header no-op match, per RFC 9111 §4.1. Both mod_cache_disk and mod_cache_socache share this one cache_select(), so one fix covers both providers.

Test: added test/modules/proxy/test_08_cache_vary_star.py, modeled on the existing test_07_cache_control.py: the original cross-client leak (Header always), a positive control proving the disk cache genuinely works absent Vary: *, and the pre-existing Header set (headers_out) case for regression coverage. The cache_select() fix itself isn't separately covered by an automated test - with the save-time gate in place no live HTTP path can produce a Vary: * disk entry anymore, and exercising the read path directly would require hand-constructing mod_cache_disk's private on-disk struct; it was instead verified with the manual two-binary reproduction above.

PR 70289


  • This PR does not report or address a security vulnerability
    (see SECURITY.md and
    https://www.apache.org/security/#reporting-a-vulnerability).
    Leaving this unchecked on purpose: this does have real cross-client
    impact, see "Impact" above. BZ 70289 was already filed as a public,
    non-embargoed Bugzilla report with a full PoC by the original
    reporter, so I'm not disclosing anything new here, but I'd rather
    flag it than self-certify.
  • Code follows the httpd style guide.
  • New log messages (level debug or higher) use an empty APLOGNO() tag;
    numbers are assigned by a committer at merge time (see docs/log-message-tags/README).
    (One new log message was added, in cache_select(); its APLOGNO() is
    left empty for a committer to assign at merge time.)
  • If the change is user-visible, a changes-entries/*.txt file is included,
    following the template in README.CHANGES (not needed otherwise).
  • Test cases based on pyhttpd are included where appropriate, in the
    test/modules/xxx directory matching the modules/xxx module source,
    or test/modules/core for core server changes. On Unix, verify these
    pass locally with e.g. make check-pytest PYTEST_DIRS=test/modules/xxx.
    (Verified both fixes against a real build with standalone end-to-end
    reproductions equivalent to this test, including the cache_select()
    upgrade scenario described above; could not run the pyhttpd suite
    itself in this sandbox, which lacks uv/the project's test venv.)

@arturobernalg
arturobernalg force-pushed the fix/httpd-70289-cache-vary-star-err-headers branch from 1ecc52a to a802ccf Compare October 31, 2026 16:08
The cacheability check only looked at r->headers_out for a Vary: *
response, so a Vary: * set via "Header always" (which writes to
r->err_headers_out) was missed. Storage itself does merge
err_headers_out into headers_out, so the response got stored and
could later be served to a different client under the same key,
handing one client's response to another.

Also fix cache_select(): it tokenized a stored response's Vary
header and, for a "*" token, looked up a request header literally
named "*". Since no such header exists on either side, both sides
were NULL and the code treated that as a match, serving a stored
Vary: * entry to a mismatched request instead of always treating
it as a mismatch per RFC 9111 4.1. This closes the read side for
any Vary: * entry already on disk (e.g. from before this fix, or
written during a rolling upgrade), the same way the save-time
check above closes the write side.

PR 70289
@arturobernalg
arturobernalg force-pushed the fix/httpd-70289-cache-vary-star-err-headers branch from a802ccf to 1d2a892 Compare October 31, 2026 16:47
@covener
covener merged commit 691e420 into apache:2.4.x Nov 1, 2026
34 of 35 checks passed
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