Skip to content

fix(cache): hash credential header values that vary puts into the key - #317

Merged
allen0099 merged 1 commit into
masterfrom
fix/vary-hash-credentials
Sep 27, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/vary-hash-credentials

Conversation

@allen0099

Copy link
Copy Markdown
Owner

@cache(vary=[...]) (unreleased, first shipping in 0.3.9) put raw header values into the cache key, so vary=["Authorization"] with cache_authorized=True, or vary=["Cookie"], wrote bearer tokens and session cookies into get_all_keys(), the /cached-records / /cached-hits monitoring routes and the Redis/Memcached keyspace.

Changes

Credential headers are hashed. For authorization, proxy-authorization, cookie and the session subsystem's default header (matched case-insensitively), a non-empty value becomes name=sha256:<64 hex>: the full SHA-256 of the normalised value (trimmed, repeated lines joined with ,, as before). It is not the 12-hex log_ref, because a key component needs collision resistance. Other headers stay readable.

  • Missing or empty headers still give the plain authorization=, not a hash of "". Anonymous callers share one entry, and the key still shows it is the anonymous one.
  • Session header name. It comes from a new DEFAULT_SESSION_HEADER_NAME constant in session/config.py, which is also the SessionConfig.header_name default. There is no circular import: the session package never imports cache.py. A session header configured under another name is not hashed, and the docs say so.
  • invalidate(vary=...) goes through the same _vary_components, so it deletes the hashed variant (tested). The monitoring routes just show the hashed component.

vary=["Cookie"] warns. A UserWarning is emitted at decoration time with stacklevel=2, so it points at the user's @cache(...) line (a test checks warning.filename). It explains the per-visitor entry growth and suggests key_builder + build_cache_key(request, <cookie or user id>) or private=True.

  • Opt-out: the standard filter, warnings.filterwarnings("ignore", message="cache vary on Cookie"), documented and tested. Skipping the warning when a key_builder is given would be wrong: the vary components are still appended to whatever the builder returns, so the per-visitor growth remains. No new parameter.
  • No warning for Authorization / X-Session-Token. One entry per caller is the intended use of vary with cache_authorized=True, and a caller keeps one token across many requests, unlike an arbitrary cookie bundle.

Interaction with the per-caller rules, documented.

  • vary=["Authorization"] does not lift the Authorization bypass. Without public/cache_authorized, only the anonymous authorization= entry is stored (tested).
  • A request with a cookie is cached, but a response that sets a cookie is still not stored and is sent private.

Docs: "Varying on request headers" in docs/HTTP_CACHING.md gains "Credential headers are hashed" and "vary=["Cookie"] warns". The @cache and invalidate docstrings and docs/CACHE_FLOW.md are updated, plus the zh-TW mirrors.

Changelog: changelog.d/268.added.md is amended rather than adding a 312.security.md. vary has never been released, so no user was exposed, and the 0.3.9 notes should describe vary's final behaviour in one place.

Tests

New tests in tests/test_cache_vary.py cover:

  • no token in the key, get_all_keys(), /cached-records or /cached-hits, and the digest is shown instead;
  • two tokens give two entries, and the same token gives a hit;
  • a missing or blank credential header gives the plain empty component;
  • the Authorization bypass still applies without the opt-in;
  • Proxy-Authorization, X-Session-Token, AUTHORIZATION casing, and joined Cookie lines;
  • non-credential headers stay readable;
  • invalidate(vary=...) deletes the hashed entry;
  • the Cookie warning's filename and text, the documented filter silencing it, and no warning for Accept-Language or the credential headers.

No existing test asserted raw credential values.

Full suite including the live Redis and Memcached tests: 1235 passed, 1 skipped, coverage 99.97%. ruff check/format and mypy --strict are clean, and pytest -k changelog passes.

Closes #312

Authorization, Proxy-Authorization, Cookie and X-Session-Token are keyed on
sha256:<hex> of the value; missing or empty stays name=. vary=['Cookie']
emits a UserWarning at decoration, since every visitor gets an entry.
@allen0099 allen0099 added this to the 0.3.9 milestone Sep 27, 2026
@allen0099
allen0099 merged commit 29c8f38 into master Sep 27, 2026
12 checks passed
@allen0099
allen0099 deleted the fix/vary-hash-credentials branch September 27, 2026 18:02
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.

@cache: hash credential header values that vary= puts into the cache key

1 participant