Skip to content

fix(cache): percent-encode | and % in cache-key host and path - #263

Merged
allen0099 merged 1 commit into
masterfrom
fix/cache-key-separator-230
Sep 26, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/cache-key-separator-230

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Summary

Closes #230.

default_key_builder joined the raw Host header and the decoded URL path with |||. Both come from the client, so either could carry the separator and shift the components. Reproduced on master:

GET /x            Host: testserver|||/p   ->  key GET|||testserver|||/p|||/x|||
GET /p%7C%7C%7C/x Host: testserver        ->  same key, served {"p": "x"}

Changes

  • fastapi_cachex/types.py: new escape_key_component (% → %25, | → %7C) and unescape_key_component. Encoding % as well keeps the mapping injective, so a path holding a literal %7C cannot collide with one holding |.
  • default_key_builder encodes the host and path. The query string is URL-encoded by str(request.query_params) already and is left alone.
  • clear_path() on the memory and Redis backends encodes its argument before matching. It still takes the path as the app sees it. Memcached is unchanged, since it only does an exact-key delete.
  • The monitoring routes (_parse_cache_key) decode host and path for display.
  • Docs:
    • HTTP_CACHING.md (EN and zh-TW): explains the encoding, how clear_pattern() interacts with it, and the one-off miss after upgrading. Recommends TrustedHostMiddleware. The per-user key builder example now uses escape_key_component.
    • CACHE_FLOW.md (EN and zh-TW): key snippet and a note on the encoding.
    • CLAUDE.md: updated.

Compatibility

Keys only change when the host or path contains | or %. Everything else hits the existing entries after the upgrade.

Tests

  • tests/test_cache_key.py::TestCacheKeySeparatorInComponents:
    • the Host-poisoning repro;
    • % encoding, built from a raw scope, because TestClient decodes the path twice;
    • ordinary keys are unchanged;
    • escape/unescape round-trips injectively;
    • the monitoring parser decodes.
  • tests/backends/test_clear_pattern_contract.py::test_clear_path_finds_paths_with_encoded_characters runs on memory and Redis.
  • Mutation-checked five reverts: builder without escaping, no % escaping, memory clear_path raw, Redis clear_path raw, routes without unescape. Each fails only the new tests aimed at it.
  • The full suite passes against live Redis and Memcached: 990 passed. Both strict docs builds pass.

CHANGELOG

Added under Unreleased → Security.

The default key builder joined the raw Host header and decoded path with
|||, so a Host of 'example.com|||/p' on GET /x stored the response under
the key of GET /p%7C%7C%7C/x. Encode | and % in both components with
escape_key_component; clear_path encodes its argument the same way and
the monitoring routes decode for display. Recommend TrustedHostMiddleware
in the HTTP caching guide.

Closes #230
@allen0099 allen0099 added this to the 0.3.8 milestone Sep 26, 2026
@allen0099 allen0099 added bug Something isn't working http-cache The @cache decorator, cache keys and Cache-Control handling security Security vulnerability or hardening labels Sep 26, 2026
@allen0099
allen0099 merged commit 4c29998 into master Sep 26, 2026
11 checks passed
@allen0099
allen0099 deleted the fix/cache-key-separator-230 branch September 26, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working http-cache The @cache decorator, cache keys and Cache-Control handling security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache key can be poisoned through the Host header: the ||| separator is not escaped

1 participant