Skip to content

Commit 5b87fc8

Browse files
committed
Harden query cache bypass and payload encoding
1 parent 24a00f7 commit 5b87fc8

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

src/Database/Database.php

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8639,16 +8639,20 @@ private function restoreQueryCacheDocuments(
86398639
* @template T
86408640
* @param string $key
86418641
* @param callable(): T $callback
8642-
* @param string $hash
8642+
* @param string|null $hash
86438643
* @return T
86448644
* @throws AuthorizationException
86458645
* @throws Exception
86468646
*/
86478647
public function withCache(
86488648
string $key,
86498649
callable $callback,
8650-
string $hash = '',
8650+
?string $hash = '',
86518651
): mixed {
8652+
if ($hash === null) {
8653+
return $callback();
8654+
}
8655+
86528656
$shouldRefreshCache = false;
86538657

86548658
try {
@@ -8684,7 +8688,10 @@ public function withCache(
86848688

86858689
if ($value !== false) {
86868690
try {
8687-
$this->cache->save($key, $this->encodeCacheValue($value), $hash);
8691+
$encoded = $this->encodeCacheValue($value);
8692+
if ($encoded !== false) {
8693+
$this->cache->save($key, $encoded, $hash);
8694+
}
86888695
} catch (Throwable $e) {
86898696
Console::warning('Warning: Failed to save cache value: ' . $e->getMessage());
86908697
}
@@ -8723,14 +8730,14 @@ private function restoreQueryCacheDocument(Document $collection, mixed $payload)
87238730
}
87248731

87258732
/**
8726-
* @return array<string, mixed>
8733+
* @return array<string, mixed>|false
87278734
*/
8728-
private function encodeCacheValue(mixed $value): array
8735+
private function encodeCacheValue(mixed $value): array|false
87298736
{
87308737
if ($value instanceof Document) {
87318738
$collection = $value->getCollection();
87328739
if ($collection === '') {
8733-
return ['value' => $value];
8740+
return false;
87348741
}
87358742

87368743
return [
@@ -8742,6 +8749,10 @@ private function encodeCacheValue(mixed $value): array
87428749

87438750
$collection = $this->getCacheValueCollection($value);
87448751
if ($collection === null || !\is_array($value)) {
8752+
if ($this->hasCacheValueDocument($value)) {
8753+
return false;
8754+
}
8755+
87458756
return ['value' => $value];
87468757
}
87478758

@@ -8772,6 +8783,25 @@ private function getCacheValueCollection(mixed $value): ?string
87728783
return null;
87738784
}
87748785

8786+
private function hasCacheValueDocument(mixed $value): bool
8787+
{
8788+
if ($value instanceof Document) {
8789+
return true;
8790+
}
8791+
8792+
if (!\is_array($value)) {
8793+
return false;
8794+
}
8795+
8796+
foreach ($value as $item) {
8797+
if ($this->hasCacheValueDocument($item)) {
8798+
return true;
8799+
}
8800+
}
8801+
8802+
return false;
8803+
}
8804+
87758805
private function encodeQueryCacheValue(mixed $value): mixed
87768806
{
87778807
if ($value instanceof Document) {

tests/unit/QueryCacheTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,64 @@ function () use (&$callbackCalls): string {
217217
$this->assertSame(2, $callbackCalls);
218218
}
219219

220+
public function testWithCacheBypassesCacheForNullHash(): void
221+
{
222+
$cache = new HashMemoryCache();
223+
$database = $this->createDatabase($cache);
224+
225+
$callbackCalls = 0;
226+
227+
$first = $database->withCache(
228+
key: 'key',
229+
callback: function () use (&$callbackCalls): string {
230+
$callbackCalls++;
231+
return 'first';
232+
},
233+
hash: null,
234+
);
235+
$second = $database->withCache(
236+
key: 'key',
237+
callback: function () use (&$callbackCalls): string {
238+
$callbackCalls++;
239+
return 'second';
240+
},
241+
hash: null,
242+
);
243+
244+
$this->assertSame('first', $first);
245+
$this->assertSame('second', $second);
246+
$this->assertSame(2, $callbackCalls);
247+
$this->assertSame([], $cache->list('key'));
248+
}
249+
250+
public function testWithCacheDoesNotCacheCollectionlessDocuments(): void
251+
{
252+
$cache = new HashMemoryCache();
253+
$database = $this->createDatabase($cache);
254+
255+
$callbackCalls = 0;
256+
257+
$first = $database->withCache(
258+
key: 'key',
259+
callback: function () use (&$callbackCalls): Document {
260+
$callbackCalls++;
261+
return new Document(['$id' => 'first']);
262+
},
263+
);
264+
$second = $database->withCache(
265+
key: 'key',
266+
callback: function () use (&$callbackCalls): Document {
267+
$callbackCalls++;
268+
return new Document(['$id' => 'second']);
269+
},
270+
);
271+
272+
$this->assertSame('first', $first->getId());
273+
$this->assertSame('second', $second->getId());
274+
$this->assertSame(2, $callbackCalls);
275+
$this->assertSame([], $cache->list('key'));
276+
}
277+
220278
public function testWithCacheCachesSingleDocument(): void
221279
{
222280
$cache = new HashMemoryCache();

0 commit comments

Comments
 (0)