Skip to content

Fix cache security hardening#101

Open
flyingsquirrel0419 wants to merge 8 commits into
mainfrom
security-hardening-docs
Open

Fix cache security hardening#101
flyingsquirrel0419 wants to merge 8 commits into
mainfrom
security-hardening-docs

Conversation

@flyingsquirrel0419

@flyingsquirrel0419 flyingsquirrel0419 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Description

Fixes and hardens cache security behavior across HTTP middleware, namespaces, DiskLayer protection, generation cleanup, telemetry, CLI safeguards, Redis invalidation, snapshots, and the docs playground. Also updates user-facing docs and adds focused regression coverage.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing behavior to change)
  • Refactor (no functional change, code improvement)
  • Documentation update
  • Test coverage improvement

Changes

  • Prevent implicit Express/Hono URL-only caching when sensitive query parameters are present unless a custom key resolver is supplied.
  • Fix namespace clear boundaries, protected DiskLayer plaintext handling, generation cleanup batching, OpenTelemetry key redaction, CLI full-cache invalidation guards, Redis URL error scrubbing, Redis invalidation unsigned warnings, and snapshot commit symlink checks.
  • Update README, SECURITY, CHANGELOG, API docs, docs-web content, localized distributed examples, and playground wording; add security regression tests and coverage-focused branch tests.

Testing

  • All existing tests pass (npm test)
  • New tests added for the changes
  • Lint passes (npm run lint)
  • Build succeeds (npm run build:all)

Additional validation run:

  • npm run test:coverage - 623 passed, 24 skipped; coverage increased to 97.22% statements / 97.60% lines
  • npm --prefix docs-web run lint
  • node --test docs-web/tests/current-docs-content.test.mjs docs-web/tests/reporting-cache-state.test.mjs docs-web/tests/run-timeout-controller.test.mjs
  • npm --prefix docs-web run build

Checklist

  • My changes follow the existing code style (single quotes, no semicolons, 2-space indent)
  • I have updated documentation where applicable
  • I have updated CHANGELOG.md and package versions when the PR changes release scope
  • I have added tests that prove my fix/feature works
  • No type errors suppressed (as any, @ts-ignore, @ts-expect-error)

Additional Notes

allowLegacyPlaintext and includeRawKeyAttributes are documented as explicit opt-ins. Unsigned Redis invalidation remains compatibility-supported but now warns through the configured logger.

Summary by CodeRabbit

Release Notes

  • Security

    • Implicit HTTP caching now bypasses requests with sensitive query parameters unless a custom keyResolver is provided.
    • DiskLayer rejects legacy plaintext entries by default; migrations can temporarily allow them.
    • Redis invalidation emits warnings in unsigned mode and hardens signed message handling; CLI redacts Redis URLs (including nested errors).
    • OpenTelemetry cache spans use hashed cache-key attributes by default.
    • Snapshot writes and parent-path validation are hardened to prevent unsafe symlink/escape cases.
  • Bug Fixes

    • Cache namespace clearing now correctly invalidates only the intended namespace range.
  • New Features

    • Browser playground now runs via a stricter sandboxed iframe runner with token-verified messaging.
  • Documentation & Tests

    • Updated security/behavior guidance and added regression coverage.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements multiple security hardening measures: HTTP middleware (Express/Hono) now bypasses implicit caching for sensitive query parameters; DiskLayer/PayloadProtection rejects legacy plaintext entries by default when encryption/signing is configured; CacheNamespace.clear() uses a delimiter-scoped prefix; snapshot atomic writes check for symlinked parent directories; OpenTelemetry spans emit layercache.key_hash instead of raw keys by default; the CLI guards --pattern '*' behind --force and masks Redis URLs in error output; RedisInvalidationBus warns when constructed without signingSecret; generation cleanup switches to streaming; and the docs playground is extracted into a sandboxed iframe/worker architecture. Tests and documentation are updated throughout.

Changes

Security Hardening, Observability, and Playground Refactor

Layer / File(s) Summary
HTTP cache sensitive-query bypass (Express & Hono)
src/integrations/httpCacheKeys.ts, src/integrations/express.ts, src/integrations/hono.ts, tests/integrations/Integrations.test.ts
inspectHttpCacheUrl refactored to return both normalizedUrl and hasSensitiveQuery; both middlewares skip implicit caching when sensitive query parameters are detected without a custom keyResolver. Integration tests assert bypass and custom-resolver re-enablement.
DiskLayer & PayloadProtection legacy plaintext rejection
src/internal/PayloadProtection.ts, src/layers/DiskLayer.ts, tests/internal/PayloadProtection.test.ts, tests/layers/DiskLayer.test.ts
allowLegacyPlaintext added to PayloadProtectionOptions and DiskLayerOptions; unprotect throws PayloadProtectionError for plaintext payloads when protection is active and opt-in is absent. Tests cover rejection, file cleanup, and migration opt-in.
CacheNamespace.clear() delimiter-scoped prefix fix
src/CacheNamespace.ts, tests/CacheNamespace.test.ts
clear() now uses namespaceKeyPrefix() returning ${prefix}: to prevent invalidating sibling namespaces; regression test validates namespace isolation.
Snapshot atomic write symlinked parent rejection
src/internal/CacheSnapshotFile.ts, src/internal/CacheStackSnapshotManager.ts, tests/internal/CacheSnapshotFile.test.ts
commitAtomicWrite validates parent directory is not a symlink and, when options provided, that real parent stays within snapshotBaseDir; tests cover normal writes and race conditions with symlink injection.
OpenTelemetry key hashing by default
src/integrations/opentelemetry.ts, tests/features/GrowthFeatures.test.ts
createOpenTelemetryPlugin gains optional options.includeRawKeyAttributes; sanitizeAttributes replaces raw layercache.key with SHA-256 layercache.key_hash by default. Tests updated for hashed attributes and opt-in path.
CLI invalidation pattern guard and Redis URL masking
src/cli.ts, tests/cli.test.ts
requiresForceForInvalidationPattern centralizes bulk-pattern detection so --pattern '*' requires --force; maskRedisUrlsInText redacts Redis URLs from nested error messages. Tests cover both behaviors.
RedisInvalidationBus unsigned-mode warning
src/invalidation/RedisInvalidationBus.ts, tests/invalidation/RedisInvalidationBus.test.ts
Constructor emits logger?.warn when signingKey is absent; tests assert warning content, non-object payload rejection, and signature-helper guard.
Generation cleanup streaming via CacheKeyDiscovery
src/internal/CacheKeyDiscovery.ts, src/CacheStack.ts, tests/internal/CacheKeyDiscovery.test.ts, tests/internal/CacheStackInternals.test.ts
forEachKeyWithPrefix refactored around shared visit/assertWithinMatchCount; cleanupGeneration streams keys incrementally and flushes batches instead of bulk-collecting.
Playground sandbox runner extraction and hardening
docs-web/lib/playground/runner-source.ts, docs-web/lib/playground/sandboxed-runner.ts, docs-web/components/playground/PlaygroundClient.tsx, tests/docs/PlaygroundWorkerSandbox.test.ts, docs-web/content/playground.mdx, docs-web/content/index.mdx
Previous inline worker removed; new runner-source.ts generates the full cache simulation worker with MockCacheStack, MockCacheLayer, deduplication, SWR, and circuit breaker; sandboxed-runner.ts wraps execution in a CSP-restricted allow-scripts iframe with per-run nonces and token-based message filtering. PlaygroundClient migrated to runPlaygroundInSandbox. Tests verify iframe sandbox, CSP, and message filtering.
Documentation, migration guide, CHANGELOG, and i18n updates
CHANGELOG.md, README.md, SECURITY.md, docs/api.md, docs/migration-guide.md, docs-web/content/docs/*, docs/i18n/README.*.md, docs-web/tests/current-docs-content.test.mjs
All hardening behaviors reflected across CHANGELOG, SECURITY.md, API docs, CLI/distributed/integrations/layers/observability/migration MDX, and i18n READMEs. Docs content tests assert new strings (bypass implicit caching, allowLegacyPlaintext, layercache.key_hash).

Sequence Diagram(s)

sequenceDiagram
  participant PlaygroundClient
  participant runPlaygroundInSandbox
  participant Iframe
  participant BlobWorker
  participant MockCacheStack

  PlaygroundClient->>runPlaygroundInSandbox: run(code, onMessage, onError)
  runPlaygroundInSandbox->>Iframe: append hidden iframe (allow-scripts sandbox, CSP srcdoc)
  Iframe-->>runPlaygroundInSandbox: load event
  runPlaygroundInSandbox->>Iframe: postMessage({code, runId, messageToken, workerSource})
  Iframe->>BlobWorker: new Worker(Blob from workerSource)
  BlobWorker->>MockCacheStack: execute user code in sandboxed globals
  BlobWorker-->>Iframe: postMessage log/done (token-filtered)
  Iframe-->>runPlaygroundInSandbox: forward to parent (runId-filtered)
  runPlaygroundInSandbox-->>PlaygroundClient: onMessage(log/done) → update UI
  PlaygroundClient->>runPlaygroundInSandbox: stop()
  runPlaygroundInSandbox->>Iframe: remove from DOM, unlisten
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • flyingsquirrel0419/layercache#2: Both PRs touch src/CacheNamespace.ts—the retrieved PR introduces CacheNamespace.clear() prefix-scoped invalidation, while the main PR later adjusts CacheNamespace.clear() to invalidate with a qualified ${prefix}: key prefix (fixing delimiter-bound prefix behavior).

Suggested reviewers

  • Ingwannu
  • turin-dev
  • famomatic

Poem

🐇 Hop hop, the cache won't sneak your token away,
Plaintext rejected, symlinks kept at bay.
A hash guards your key, the namespace won't bleed,
The playground's sandboxed — no mischief shall proceed!
With --force now required when you wipe it all clean,
The rabbit signs off: security supreme! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Fix cache security hardening' accurately summarizes the primary objective of implementing comprehensive cache security hardening across the codebase, including sensitive query parameter handling, namespace boundaries, DiskLayer protection, and other security fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security-hardening-docs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-web/content/docs/migration.mdx`:
- Around line 34-36: The "Implicit HTTP cache keys" section in the migration
guide references "common sensitive query parameters" without providing the
complete enumeration or linking to where they are documented. Add either a
reference link to the integrations.mdx Express middleware Options section where
the full sensitive parameter list is specified (access_token, api_key, apikey,
auth, authorization, code, credentials, id_token, jwt, password, private_key,
refresh_token, secret, session, sessionid, session_id, token), or include the
complete list directly in this migration section to ensure users can understand
the full scope of parameters being handled without needing to consult additional
documentation.

In `@src/integrations/httpCacheKeys.ts`:
- Around line 44-48: The catch block that handles URL parsing failures currently
returns hasSensitiveQuery: false, which allows implicit caching when URL
inspection fails and creates a security risk. Change the return value of
hasSensitiveQuery from false to true in the error handler so that requests where
URL parsing fails are treated as sensitive by default, ensuring the middleware
bypasses implicit caching as a fail-closed security measure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4706502d-ef4a-4e7f-ad60-df4d36002ddb

📥 Commits

Reviewing files that changed from the base of the PR and between e5107fe and e5629b6.

📒 Files selected for processing (46)
  • CHANGELOG.md
  • README.md
  • SECURITY.md
  • docs-web/components/playground/PlaygroundClient.tsx
  • docs-web/content/docs/api.mdx
  • docs-web/content/docs/cli.mdx
  • docs-web/content/docs/distributed.mdx
  • docs-web/content/docs/index.mdx
  • docs-web/content/docs/integrations.mdx
  • docs-web/content/docs/layers.mdx
  • docs-web/content/docs/migration.mdx
  • docs-web/content/docs/observability.mdx
  • docs-web/content/index.mdx
  • docs-web/content/playground.mdx
  • docs-web/lib/playground/worker-sandbox.ts
  • docs-web/lib/playground/worker.ts
  • docs-web/tests/current-docs-content.test.mjs
  • docs/api.md
  • docs/i18n/README.es.md
  • docs/i18n/README.ja.md
  • docs/i18n/README.ko.md
  • docs/i18n/README.zh-CN.md
  • docs/migration-guide.md
  • src/CacheNamespace.ts
  • src/CacheStack.ts
  • src/cli.ts
  • src/integrations/express.ts
  • src/integrations/hono.ts
  • src/integrations/httpCacheKeys.ts
  • src/integrations/opentelemetry.ts
  • src/internal/CacheKeyDiscovery.ts
  • src/internal/CacheSnapshotFile.ts
  • src/internal/PayloadProtection.ts
  • src/invalidation/RedisInvalidationBus.ts
  • src/layers/DiskLayer.ts
  • tests/CacheNamespace.test.ts
  • tests/cli.test.ts
  • tests/docs/PlaygroundWorkerSandbox.test.ts
  • tests/features/GrowthFeatures.test.ts
  • tests/integrations/Integrations.test.ts
  • tests/internal/CacheKeyDiscovery.test.ts
  • tests/internal/CacheSnapshotFile.test.ts
  • tests/internal/CacheStackInternals.test.ts
  • tests/internal/PayloadProtection.test.ts
  • tests/invalidation/RedisInvalidationBus.test.ts
  • tests/layers/DiskLayer.test.ts

Comment thread docs-web/content/docs/migration.mdx
Comment thread src/integrations/httpCacheKeys.ts
@flyingsquirrel0419 flyingsquirrel0419 force-pushed the security-hardening-docs branch from 8ae9bd7 to ee9249e Compare June 19, 2026 07:12
@flyingsquirrel0419

flyingsquirrel0419 commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@flyingsquirrel0419 Sure! I'll kick off a review of this PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@flyingsquirrel0419 Sure! I'll kick off a review of this PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flyingsquirrel0419 flyingsquirrel0419 self-assigned this Jun 20, 2026
@flyingsquirrel0419

Copy link
Copy Markdown
Owner Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@flyingsquirrel0419 Sure! I'll kick off a full review of this PR right away.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flyingsquirrel0419

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/internal/CacheKeyDiscovery.ts (2)

14-22: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward maxMatches to enable early termination and prevent redundant limit checking.

The maxMatches parameter is not passed to forEachKeyWithPrefix on line 16, which defeats the streaming optimization. The internal visit function in forEachKeyWithPrefix is designed to enforce the limit during key discovery, but without the parameter, it uses the default false (no limit). This causes:

  1. Memory waste: When the tag index uses non-streaming APIs (keysForPrefix/matchPattern), all matching keys are materialized into an array before any limit check.
  2. CPU waste: Layer scanning continues even after the limit is logically exceeded.
  3. Redundant checking: The visitor callback checks the limit again, but this happens too late.

The visitor check on line 18 also becomes redundant if maxMatches is properly forwarded, since the internal visit function will throw before the visitor is called for the exceeding key.

⚡ Proposed fix
 async collectKeysWithPrefix(prefix: string, maxMatches: number | false = false): Promise<string[]> {
   const matches = new Set<string>()
-  await this.forEachKeyWithPrefix(prefix, (key) => {
+  await this.forEachKeyWithPrefix(prefix, (key) => {
     matches.add(key)
-    this.assertWithinMatchLimit(matches, maxMatches)
-  })
+  }, maxMatches)

   return [...matches]
 }

Note: The assertWithinMatchLimit call in the visitor becomes unreachable and can be removed, since the internal visit function will throw before the visitor is called when the limit is exceeded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/internal/CacheKeyDiscovery.ts` around lines 14 - 22, The
`collectKeysWithPrefix` method is not forwarding the `maxMatches` parameter to
the `forEachKeyWithPrefix` call, which prevents early termination and causes
inefficient resource usage. Pass the `maxMatches` parameter directly to the
`forEachKeyWithPrefix` method so that the internal `visit` function can enforce
the limit during key discovery. Additionally, remove the now-redundant
`assertWithinMatchLimit` call from the visitor callback since the internal visit
function will throw before the visitor callback is invoked when the limit is
exceeded.

137-141: ⚠️ Potential issue | 🟡 Minor

Remove unused private method assertWithinMatchCount.

The assertWithinMatchCount method at lines 137–141 is defined but never called anywhere in the codebase. Since it's marked private, it cannot be invoked from outside the class, making this a safe removal.

Cleanup
-  private assertWithinMatchCount(matches: number, maxMatches: number | false): void {
-    if (maxMatches !== false && matches > maxMatches) {
-      throw new Error(`Invalidation matched too many keys (${matches} > ${maxMatches}).`)
-    }
-  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/internal/CacheKeyDiscovery.ts` around lines 137 - 141, Remove the unused
private method `assertWithinMatchCount` from the CacheKeyDiscovery class. This
method is never called anywhere in the codebase and can be safely deleted.
Simply remove the entire method definition including the method signature and
its body.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/internal/CacheKeyDiscovery.ts`:
- Around line 14-22: The `collectKeysWithPrefix` method is not forwarding the
`maxMatches` parameter to the `forEachKeyWithPrefix` call, which prevents early
termination and causes inefficient resource usage. Pass the `maxMatches`
parameter directly to the `forEachKeyWithPrefix` method so that the internal
`visit` function can enforce the limit during key discovery. Additionally,
remove the now-redundant `assertWithinMatchLimit` call from the visitor callback
since the internal visit function will throw before the visitor callback is
invoked when the limit is exceeded.
- Around line 137-141: Remove the unused private method `assertWithinMatchCount`
from the CacheKeyDiscovery class. This method is never called anywhere in the
codebase and can be safely deleted. Simply remove the entire method definition
including the method signature and its body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a324ac33-a93f-4c03-8a25-ab81e2c68fc5

📥 Commits

Reviewing files that changed from the base of the PR and between b6a44dc and ca21b8a.

📒 Files selected for processing (5)
  • src/internal/CacheKeyDiscovery.ts
  • src/internal/CacheSnapshotFile.ts
  • src/internal/CacheStackSnapshotManager.ts
  • tests/internal/CacheKeyDiscovery.test.ts
  • tests/internal/CacheSnapshotFile.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/internal/CacheStackSnapshotManager.ts
  • tests/internal/CacheSnapshotFile.test.ts
  • src/internal/CacheSnapshotFile.ts

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.

2 participants