| Version | Supported |
|---|---|
| 0.1.x | Yes |
Do not open a public GitHub issue for security vulnerabilities.
Instead, please report security issues via:
- Email: security@cachekit.io
- GitHub Security Advisories: Report a vulnerability
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
| Stage | Timeline |
|---|---|
| Initial response | 48 hours |
| Triage & assessment | 7 days |
| Fix development | 14-30 days |
| Public disclosure | After fix released |
| Component | Algorithm | Notes |
|---|---|---|
| Encryption | AES-256-GCM | AEAD via ring crate |
| Key Derivation | HKDF-SHA256 | RFC 5869 compliant |
| Integrity | xxHash3-64 | Non-cryptographic (corruption detection) |
| Nonce | Counter + Random IV | Unique per encryption |
This crate protects against:
- Data tampering: GCM authentication tags (when encryption enabled); xxHash3 detects accidental corruption only
- Data disclosure: AES-256-GCM encryption (when enabled)
- Key compromise isolation: HKDF domain separation per tenant
- Decompression bombs: Size limits + ratio validation (see Decompression limits)
- Memory disclosure:
zeroizeon drop for key material
This crate does not protect against:
- Side-channel attacks on the host system
- Compromise of the master key
- Denial of service via resource exhaustion (partial protection only)
- Attacks requiring physical access
StorageEnvelope::extract bounds LZ4 decompression before calling
lz4_flex::decompress, so a forged envelope cannot expand without limit:
| Limit | Value | Enforced on |
|---|---|---|
MAX_COMPRESSED_SIZE |
512 MiB | compressed_data.len() |
MAX_UNCOMPRESSED_SIZE |
512 MiB | declared original_size |
MAX_COMPRESSION_RATIO |
1000:1 | original_size vs compressed_data.len() |
The same MAX_COMPRESSED_SIZE constant also bounds the serialized envelope
before MessagePack deserialization — but that check lives in
ByteStorage::retrieve and ByteStorage::validate, not on StorageEnvelope.
StorageEnvelope is public with public fields, so a caller who deserializes it
directly gets no such bound and must impose one.
The ratio product is computed in u64 via checked_mul, and overflow is
treated as a bomb. Zero-length compressed data is rejected unconditionally,
including when the envelope declares original_size == 0.
lz4_flex returns OutputTooSmall rather than growing past the allocation, so
the decompressed output is bounded by min(512 MiB, 1000 × compressed_data.len())
regardless of what the envelope claims. extract then re-checks the produced
length, because a decompressor's size argument sizes a buffer; it never asserts
the decoded length. Keep lz4_flex's default safe-decode feature on: it
decodes into a zero-filled fixed-length buffer, where the non-safe decoder uses
with_capacity + set_len.
original_sizedoes not act as a bound. It is attacker-controlled on any backend an attacker can write to. It does size the allocation, but only within the absolute and ratio limits already checked above — so a forged envelope can still make a reader allocate up to1000 × compressed_data.len()before the LZ4 stream is validated. That is allocation amplification within the bound, not a bypass of it. Note thatByteStorage::validate()reaches the same allocation — it callsextract()and discards the result — so it is not a cheap structural pre-screen for untrusted envelopes.- xxHash3-64 is not a control here. It is unkeyed, so anyone who can forge an envelope recomputes it. It detects accidental corruption, not forgery. Authentication comes from AES-256-GCM, and only for secure caches.
The ceiling is server-class. ByteStorage::retrieve holds the serialized
envelope, the deserialized compressed_data copy and the decompressed output at
the same time, so a single call can peak well above 512 MiB — up to roughly
1.5 GiB at the limits. That does not fit a constrained runtime: a Cloudflare
Workers isolate has ~128 MiB, so a payload well inside these limits can still
exhaust it, and on wasm32 the allocation is an eager memory.grow that needs
no valid LZ4 stream behind it. On wasm32 that is worse than a spike: linear
memory never shrinks, so a single large extract permanently raises the
isolate's floor for every subsequent request it serves. Deployments on
constrained runtimes must bound payload size at the caller. Making these
constants environment-aware or configurable is tracked as a follow-up.
Test coverage. The unit tests in src/byte_storage.rs call extract
directly and are the only merge-time enforcement of the bound. The
compression_bomb fuzz target (fuzz/fuzz_targets/compression_bomb.rs) is a
build-and-smoke check at pull-request time, and its weekly deep run restarts
from an empty corpus. The Kani harnesses never run on pull requests, never
execute StorageEnvelope::extract, and cannot detect a wrong predicate. Treat both as
smoke checks, not as verification of the bound.
Security-critical dependencies are audited via cargo-deny:
cargo deny check advisoriesSee deny.toml for the full security policy.
A CycloneDX 1.6 SBOM is generated by cargo-sbom during publish and attested
against the packaged crate via
actions/attest-sbom. The attestation
is the verifiable artifact — verify it against the crate as published:
# Download the published crate, then verify the SBOM attestation against it.
curl -sSLO https://static.crates.io/crates/cachekit-core/cachekit-core-0.4.0.crate
gh attestation verify cachekit-core-0.4.0.crate --repo cachekit-io/cachekit-core \
--predicate-type https://cyclonedx.org/bomNote the predicate type carries no version suffix: actions/attest-sbom records
CycloneDX as https://cyclonedx.org/bom regardless of spec version (the version
lives in the document's own specVersion). Provenance is attested separately
under https://slsa.dev/provenance/v1 against the same subject.
GitHub releases for this repository carry no SBOM file as a downloadable asset. Immutable releases are enabled here, which seals a release's assets at publish time, so an SBOM cannot be attached after the fact. Use the attestation above.
No vulnerabilities have been disclosed yet.