fix(crusher): switch compression_store cache key from MD5 to SHA-256 (CodeQL #395)

CodeQL flagged `compression_store.store()`'s default MD5 cache key as
`py/weak-sensitive-data-hashing` after the explicit_hash refactor in
PR #395 brought the line into a new diff context.

First attempt: `usedforsecurity=False` + `# lgtm[...]` comment to
silence the alert without changing the hash. Both failed — CodeQL
ignores the hashlib parameter and our LGTM marker, the alert stayed
open on the PR.

Second attempt (this commit): drop MD5 entirely. The cache key is for
deduplication / lookup, not security or integrity, so any deterministic
function works. SHA-256[:24] gives the same 96-bit collision space as
MD5[:24] (~280 trillion entries for 50% collision under birthday
bound), is FIPS-clean, and CodeQL won't flag it.

Behaviour impact: zero. The cache is in-memory (no disk persistence),
so the same content always hashes deterministically under whichever
function is in use — there is no upgrade-time mismatch to manage.

Drive-by: pre-commit's sync-plugin-versions hook bumped marketplace +
plugin manifests to 0.20.28 since v0.20.27 was tagged on main.
This commit is contained in:
chopratejas 2026-05-05 12:28:22 -07:00
parent a40d4a5f6d
commit af9e6282f8
5 changed files with 23 additions and 16 deletions

View file

@ -5,14 +5,14 @@
},
"metadata": {
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
"version": "0.20.16"
"version": "0.20.28"
},
"plugins": [
{
"name": "headroom",
"source": "./plugins/headroom-agent-hooks",
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
"version": "0.20.16",
"version": "0.20.28",
"author": {
"name": "Headroom Contributors",
"url": "https://github.com/chopratejas/headroom"

View file

@ -5,14 +5,14 @@
},
"metadata": {
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
"version": "0.20.16"
"version": "0.20.28"
},
"plugins": [
{
"name": "headroom",
"source": "./plugins/headroom-agent-hooks",
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
"version": "0.20.16",
"version": "0.20.28",
"author": {
"name": "Headroom Contributors",
"url": "https://github.com/chopratejas/headroom"

View file

@ -199,7 +199,7 @@ class CompressionStore:
compression_strategy: Strategy used for compression.
ttl: Custom TTL in seconds (uses default if not specified).
explicit_hash: Use this exact hex hash as the storage key
instead of computing MD5(original)[:24]. Required when
instead of computing SHA-256(original)[:24]. Required when
the marker that points at this entry was emitted by a
producer with its own hash function (e.g. SmartCrusher's
Rust row-drop path uses SHA-256[:12]). If not a hex
@ -210,28 +210,35 @@ class CompressionStore:
Returns:
Hash key for retrieving this content.
"""
# Generate hash from original content. Default: MD5[:24] of the
# Generate hash from original content. Default: SHA-256[:24] of the
# original. When the caller provides `explicit_hash`, use it
# verbatim — required when the hash that ends up in the prompt
# marker is produced by another component (e.g. the Rust
# SmartCrusher row-drop path emits SHA-256[:12], which the
# Python store has to mirror so /v1/retrieve resolves it).
# CRITICAL FIX #5: Use 24 chars (96 bits) instead of 16 (64 bits) for better
# collision resistance. Birthday paradox: 50% collision at sqrt(2^n) entries.
# - 64 bits: ~4 billion entries for 50% collision
# - 96 bits: ~280 trillion entries for 50% collision
# 24 chars (96 bits) was chosen for collision resistance under the
# birthday bound: 50% collision probability at ~280 trillion entries
# (2^48), versus ~4 billion (2^32) for the previous 16-char default.
if explicit_hash is not None:
# Validate as hex. Bail loudly per `feedback_no_silent_fallbacks`
# — silently falling back to MD5 when the caller asked for a
# specific key would defeat the marker/store consistency we're
# trying to preserve.
# — silently falling back to the default hash when the caller
# asked for a specific key would defeat the marker/store
# consistency we're trying to preserve.
if not explicit_hash or not all(c in "0123456789abcdefABCDEF" for c in explicit_hash):
raise ValueError(
f"explicit_hash must be a non-empty hex string, got {explicit_hash!r}"
)
hash_key = explicit_hash.lower()
else:
hash_key = hashlib.md5(original.encode()).hexdigest()[:24] # nosec B324
# SHA-256 truncated to 24 hex chars (96 bits) — same collision
# space as the MD5[:24] this replaced. Switched from MD5 in
# PR #395 to silence CodeQL's `py/weak-sensitive-data-hashing`
# rule (the `usedforsecurity=False` parameter and the `lgtm`
# comment marker both failed to suppress it). The cache is
# in-memory, so changing the hash function on upgrade has no
# persistence-side effect — the same content always hashes
# deterministically under whichever function is in use.
hash_key = hashlib.sha256(original.encode()).hexdigest()[:24]
entry = CompressionEntry(
hash=hash_key,

View file

@ -1,6 +1,6 @@
{
"name": "headroom",
"version": "0.20.16",
"version": "0.20.28",
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
"author": {
"name": "Headroom Contributors",

View file

@ -1,6 +1,6 @@
{
"name": "headroom",
"version": "0.20.16",
"version": "0.20.28",
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
"author": {
"name": "Headroom Contributors",