Skip to content

Commit bc6bc33

Browse files
Berik AshimovBerik Ashimov
authored andcommitted
Security hardening (v0.2.1)
- Cache backend get/set failures degrade to cache miss/uncached instead of 500 (CWE-703) - Interpolated cache tag values length-capped before use as Redis keys (CWE-770)
1 parent 9b9ba5f commit bc6bc33

4 files changed

Lines changed: 48 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 0.2.1 — 2026-06-10
4+
5+
Security hardening.
6+
7+
### Changed
8+
9+
- Cache backend `get` / `set` failures (e.g. a Redis outage) are now caught and
10+
logged, degrading gracefully to a cache miss / uncached response instead of
11+
surfacing a 500 (CWE-703).
12+
- Interpolated cache tag values are length-capped (256 chars) before use as
13+
Redis key components, so a hostile path segment cannot inflate the tag-key
14+
namespace (CWE-770).
15+
316
## [0.2.0] - 2026-05-16
417

518
Security hardening.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "hawkapi-cache"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "Response caching for HawkAPI — decorator + middleware + Redis/memory backends + tag-based invalidation"
99
readme = "README.md"
1010
license = { file = "LICENSE" }

src/hawkapi_cache/_decorator.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,17 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
9292
)
9393
cache_key = plugin.key_prefix + base_key
9494

95-
cached = await plugin.backend.get(cache_key)
95+
try:
96+
cached = await plugin.backend.get(cache_key)
97+
except Exception as exc:
98+
# Backend outage (e.g. Redis down) — fail open: treat as a
99+
# cache miss and serve from the handler rather than 500.
100+
logger.warning(
101+
"cache: backend get failed for key %s, treating as miss: %s",
102+
cache_key,
103+
exc,
104+
)
105+
cached = None
96106
decoded: tuple[int, list[tuple[bytes, bytes]], bytes] | None = None
97107
if cached is not None:
98108
try:
@@ -124,12 +134,22 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
124134

125135
interpolated = interpolate_tags(tags, dict(request.path_params))
126136
raw_headers = response._build_raw_headers() # pyright: ignore[reportPrivateUsage]
127-
await plugin.backend.set(
128-
cache_key,
129-
encode(response.status_code, raw_headers, response.body),
130-
ttl=ttl,
131-
tags=interpolated,
132-
)
137+
try:
138+
await plugin.backend.set(
139+
cache_key,
140+
encode(response.status_code, raw_headers, response.body),
141+
ttl=ttl,
142+
tags=interpolated,
143+
)
144+
except Exception as exc:
145+
# Backend outage (e.g. Redis down) — fail open: serve the
146+
# response uncached rather than 500.
147+
logger.warning(
148+
"cache: backend set failed for key %s, serving uncached: %s",
149+
cache_key,
150+
exc,
151+
)
152+
return response
133153
response._headers["x-cache"] = "MISS" # pyright: ignore[reportPrivateUsage]
134154
return response
135155

src/hawkapi_cache/_key.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
if TYPE_CHECKING:
99
from collections.abc import Sequence
1010

11+
# Cap interpolated path-param values so a hostile path cannot inflate the
12+
# Redis tag-key namespace with unbounded-length keys.
13+
_MAX_TAG_LEN = 256
14+
1115

1216
def make_key(
1317
method: str,
@@ -27,10 +31,12 @@ def make_key(
2731

2832
def interpolate_tags(tags: Sequence[str], path_params: dict[str, Any]) -> list[str]:
2933
"""Substitute ``{name}`` placeholders in tag strings with path-param values."""
34+
# Cap each value so a hostile path segment cannot produce an unbounded tag key.
35+
capped = {k: str(v)[:_MAX_TAG_LEN] for k, v in path_params.items()}
3036
out: list[str] = []
3137
for tag in tags:
3238
try:
33-
out.append(tag.format(**path_params))
39+
out.append(tag.format(**capped))
3440
except (KeyError, IndexError):
3541
# Unknown placeholder — keep tag as-is rather than failing the request.
3642
out.append(tag)

0 commit comments

Comments
 (0)