mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
fix(cache): verify md5 signature on load, drop magic-byte check
Replace the output-side magic-byte check with a read-time integrity verification in the shared cache middleware. The cache document already stores `signature = md5(payload)` at save time, but the load path never verified it. If the filesystem blob and the cache document drift — a partial write, disk corruption, or a bad entry that predates the content-type save guard — the mismatched md5 surfaces here and we treat the hit as a miss, purge, and let the action regenerate the response. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
37d6d33814
commit
957a588786
@@ -640,6 +640,21 @@ Http::init()
|
||||
$timestamp = 60 * 60 * 24 * 180; // Temporarily increase the TTL to 180 day to ensure files in the cache are still fetched.
|
||||
$data = $cache->load($key, $timestamp);
|
||||
|
||||
// Integrity check: the filesystem blob and the cache document are
|
||||
// two separate stores. If one drifts (partial write, disk corruption,
|
||||
// a bad entry that slipped past the save-time content-type guard),
|
||||
// the md5 we signed at save time will no longer match. Treat as a
|
||||
// miss and purge so the action re-runs and writes a fresh entry.
|
||||
if (! empty($data) && ! $cacheLog->isEmpty()) {
|
||||
$storedSignature = $cacheLog->getAttribute('signature', '');
|
||||
if (! empty($storedSignature) && \md5($data) !== $storedSignature) {
|
||||
$cache->purge($key);
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'corrupt']);
|
||||
Span::add('storage.cache.corrupt', true);
|
||||
$data = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($data) && ! $cacheLog->isEmpty()) {
|
||||
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
|
||||
$type = $parts[0];
|
||||
|
||||
@@ -278,15 +278,6 @@ class Get extends Action
|
||||
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
// Defend against Imagick producing a malformed or empty buffer from a
|
||||
// partial/corrupted source. The shutdown hook caches any 2xx response
|
||||
// with an image content-type, so a degenerate payload would poison the
|
||||
// cache for the full TTL. Verify the magic bytes match the declared
|
||||
// output format before handing the blob off to the response.
|
||||
if (!self::hasExpectedMagicBytes($data, $output)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED, 'Rendered preview failed integrity check');
|
||||
}
|
||||
|
||||
$renderingTime = \microtime(true) - $startTime - $downloadTime - $decryptionTime - $decompressionTime;
|
||||
|
||||
$totalTime = \microtime(true) - $startTime;
|
||||
@@ -323,22 +314,4 @@ class Get extends Action
|
||||
|
||||
unset($image);
|
||||
}
|
||||
|
||||
private static function hasExpectedMagicBytes(string $data, string $output): bool
|
||||
{
|
||||
if (\strlen($data) < 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = \strtolower($output);
|
||||
return match ($format) {
|
||||
'jpg', 'jpeg' => \str_starts_with($data, "\xFF\xD8\xFF"),
|
||||
'png' => \str_starts_with($data, "\x89PNG\r\n\x1A\n"),
|
||||
'gif' => \str_starts_with($data, 'GIF87a') || \str_starts_with($data, 'GIF89a'),
|
||||
'webp' => \str_starts_with($data, 'RIFF') && \substr($data, 8, 4) === 'WEBP',
|
||||
// Unknown/unsupported output formats — skip the check rather than
|
||||
// reject, so new formats added to storage-outputs don't silently fail.
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user