Skip to content

Fix config state cache poisoning#827

Merged
abnegate merged 8 commits intomainfrom
fix-cache
Mar 6, 2026
Merged

Fix config state cache poisoning#827
abnegate merged 8 commits intomainfrom
fix-cache

Conversation

@abnegate
Copy link
Member

@abnegate abnegate commented Mar 6, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Document-level cache keys now incorporate normalized selections (sorted), relationship-resolution status, and the set of enabled filters (using stable filter identifiers). The resulting identifier is appended to the document key when a document ID is present, replacing the prior behavior that considered selections only.
  • Improvements
    • Filters are now identified by stable signatures, reducing cache collisions and improving cache correctness when filters are added or registered at runtime.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

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

Added stable callable signature computation and store filter signatures on registration and construction; getCacheKeys now includes enabled filter signatures (with sorted selects and relationships flag) in the per-document payload which is MD5-hashed for the document key; behavior unchanged when no documentId.

Changes

Cohort / File(s) Summary
Database core
src/Database/Database.php
Added computeCallableSignature(callable) and store a signature for each filter (constructor and addFilter). Updated getCacheKeys (when documentId present) to build a payload of sorted selects, relationshipsResolved, and enabled filter signatures, then use md5(json_payload) for the document hash key (no change when no document parts exist).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I sniff the callables, trace each name,
I sort the selects and tuck them in frame,
I pack filters by signature, neat and small,
I hash them together and hop down the hall,
Cache keys snug — I celebrate the change with a thump and a paw ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix config state cache poisoning' accurately describes the main change, which involves fixing cache key computation to include filter signatures and other state to prevent cache poisoning issues.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-cache

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Database.php`:
- Around line 9215-9230: The current flattening of $configParts into a
colon-separated string can produce ambiguous collisions (e.g. ['a,b'] vs
['a','b']); replace the ad-hoc separator serialization with an unambiguous
encoder (e.g. json_encode or serialize) when building the config portion added
to $hashParts so each element ($this->filter, $this->resolveRelationships,
sorted \array_keys($this->disabledFilters)) is encoded in a non-ambiguous,
stable form before you run \md5 on \implode('|', $hashParts) to create
$documentHashKey; ensure you keep the existing sort of $this->disabledFilters
and use the same encoder for all entries so $documentHashKey uniquely represents
the config state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 208233e4-443d-4d4c-8897-508e1f40e94f

📥 Commits

Reviewing files that changed from the base of the PR and between 8227f57 and 6b61927.

📒 Files selected for processing (1)
  • src/Database/Database.php

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/Database/Database.php (1)

9211-9230: ⚠️ Potential issue | 🟠 Major

Serialize the selects payload structurally too.

$hashParts[] = \implode($selects); is still collision-prone: ['ab', 'c'] and ['a', 'bc'] both hash as abc. That means two different projections can still share a documentHashKey and return the wrong cached document. Please encode the full payload as structured data instead of concatenated strings.

🛠️ Proposed fix
-            $hashParts = [];
+            $hashPayload = [];

             if (!empty($selects)) {
-                $hashParts[] = \implode($selects);
+                $hashPayload['selects'] = \array_values($selects);
             }

             // Include non-default config state to prevent cache poisoning when
             // documents are fetched with filters/relationships disabled
             if (!$this->filter || !$this->resolveRelationships || !empty($this->disabledFilters)) {
-                $configParts = [$this->filter ? '1' : '0', $this->resolveRelationships ? '1' : '0'];
+                $disabled = [];

                 if (!empty($this->disabledFilters)) {
-                    $disabled = \array_keys($this->disabledFilters);
+                    $disabled = \array_keys($this->disabledFilters);
                     \sort($disabled);
-                    $configParts[] = \json_encode($disabled);
                 }

-                $hashParts[] = \json_encode($configParts);
+                $hashPayload['config'] = [
+                    'filter' => $this->filter,
+                    'resolveRelationships' => $this->resolveRelationships,
+                    'disabledFilters' => $disabled,
+                ];
             }

-            if (!empty($hashParts)) {
-                $documentHashKey = $documentKey . ':' . \md5(\implode('|', $hashParts));
+            if (!empty($hashPayload)) {
+                $documentHashKey = $documentKey . ':' . \md5(\json_encode($hashPayload));
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 9211 - 9230, The selects list is
currently concatenated with implode which can cause collisions (e.g., ['ab','c']
vs ['a','bc']); in the block that builds $hashParts (the code that currently
does $hashParts[] = \implode($selects);), replace that concatenation with a
structural, deterministic encoding such as sorting $selects (if order is not
significant) and then adding $hashParts[] = \json_encode($selects) (or sort +
json_encode to ensure stable ordering) so the documentHashKey (used when
building $documentHashKey) uniquely represents the selects payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Database/Database.php`:
- Around line 9211-9230: The selects list is currently concatenated with implode
which can cause collisions (e.g., ['ab','c'] vs ['a','bc']); in the block that
builds $hashParts (the code that currently does $hashParts[] =
\implode($selects);), replace that concatenation with a structural,
deterministic encoding such as sorting $selects (if order is not significant)
and then adding $hashParts[] = \json_encode($selects) (or sort + json_encode to
ensure stable ordering) so the documentHashKey (used when building
$documentHashKey) uniquely represents the selects payload.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 925d14cc-1f5c-4fec-bcac-8b1ccda42c4a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b61927 and e8b197a.

📒 Files selected for processing (1)
  • src/Database/Database.php

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Database.php`:
- Around line 9209-9229: The current key-building logic concatenates fragments
into $hashParts and implodes them which can cause select collisions and leaves
the empty-check for $hashParts pointless; instead build a single structured
payload (include $selects, the resolved relationships flag
$this->resolveRelationships, and the computed $enabled filter list derived from
self::$filters and $this->instanceFilters minus $this->disabledFilters)
JSON-encode that payload and use md5(json_payload) when forming $documentHashKey
from $documentKey, eliminating the fragment implode approach using $hashParts
and the now-dead empty check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dca13bfd-3a36-4def-87da-ca1676181ea4

📥 Commits

Reviewing files that changed from the base of the PR and between e8b197a and 2b002dc.

📒 Files selected for processing (1)
  • src/Database/Database.php

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Database.php`:
- Around line 9212-9225: The cache payload currently only includes bare filter
names ($enabled) which ignores provenance (instance vs static) used by
decodeAttribute() and can cause collisions when $this->instanceFilters overrides
self::$filters; change the payload construction so "filters" is a mapping of
filter name to a provenance/signature (e.g. mark whether it comes from
$this->instanceFilters or self::$filters and include a stable identifier for
that implementation such as the callable's class::method, function name, or a
hash of the closure/object) instead of a plain list; update the code that builds
$enabled and the json_encode payload to produce that name => signature map so
cache keys reflect the effective filter implementation used by
decodeAttribute().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc6578ae-d9c7-4204-bdc8-8fd27dc7491a

📥 Commits

Reviewing files that changed from the base of the PR and between 2b002dc and 0c48269.

📒 Files selected for processing (1)
  • src/Database/Database.php

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Database.php`:
- Around line 474-478: The internal filter arrays now include a 'signature' key
but the PHPDoc shapes were not updated, causing PHPStan failures; update the
docblocks for self::$filters, $this->instanceFilters, the __construct()
parameter $filters, and the getInstanceFilters() return to reflect the new shape
{encode: callable, decode: callable, signature: string} (and likewise update any
other docblocks referencing the old shape such as those near getCacheKeys() and
the alternate filter declarations around the other filter initialization block),
ensuring all annotations use the exact key names and types so references like
$callbacks['signature'] are type-checked correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a0d4a608-dfe8-439f-97d1-3090ca90ce82

📥 Commits

Reviewing files that changed from the base of the PR and between 0c48269 and 0c39efe.

📒 Files selected for processing (1)
  • src/Database/Database.php

Comment on lines +474 to 478
foreach ($filters as $name => $callbacks) {
$filters[$name]['signature'] = self::computeCallableSignature($callbacks['encode'])
. ':' . self::computeCallableSignature($callbacks['decode']);
}
$this->instanceFilters = $filters;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update the internal filter array-shapes to include signature.

These assignments change both filter stores from {encode, decode} to {encode, decode, signature}, but the class/property/param/return annotations still declare the old shape. That is why PHPStan is already failing on Lines 9228 and 9235 when getCacheKeys() reads $callbacks['signature']. Please extend the shapes for self::$filters, $this->instanceFilters, __construct()'s $filters, and getInstanceFilters().

Also applies to: 8612-8618

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Database.php` around lines 474 - 478, The internal filter arrays
now include a 'signature' key but the PHPDoc shapes were not updated, causing
PHPStan failures; update the docblocks for self::$filters,
$this->instanceFilters, the __construct() parameter $filters, and the
getInstanceFilters() return to reflect the new shape {encode: callable, decode:
callable, signature: string} (and likewise update any other docblocks
referencing the old shape such as those near getCacheKeys() and the alternate
filter declarations around the other filter initialization block), ensuring all
annotations use the exact key names and types so references like
$callbacks['signature'] are type-checked correctly.

@abnegate abnegate merged commit 489e3ce into main Mar 6, 2026
18 checks passed
@abnegate abnegate deleted the fix-cache branch March 6, 2026 08:21
@coderabbitai coderabbitai bot mentioned this pull request Mar 6, 2026
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.

1 participant