Merge pull request #7145 from shleeable/fix/media-blocklist-check-before-store

Check media blocklist before storing uploads to prevent orphaned files
pull/7147/head
Shlee 2 weeks ago committed by GitHub
commit 0ebe1f8fb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -267,9 +267,14 @@ class ApiV2Controller extends Controller
abort(403, 'Invalid or unsupported mime type.');
}
// Check the blocklist against the temp upload BEFORE storing, so a
// blocked upload never leaves an orphaned file on disk (media:gc only
// reaps files that have a Media row).
$hash = \hash_file('sha256', $photo->getRealPath());
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$storagePath = MediaPathService::get($user, 2);
$path = $photo->storePublicly($storagePath);
$hash = \hash_file('sha256', $photo);
$license = null;
$mime = $photo->getMimeType();
@ -283,8 +288,6 @@ class ApiV2Controller extends Controller
}
}
abort_if(MediaBlocklistService::exists($hash) == true, 451);
if ($request->has('replace_id')) {
$rpid = $request->input('replace_id');
$removeMedia = Media::whereNull('status_id')

@ -107,12 +107,15 @@ class ComposeController extends Controller
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
// Check the blocklist against the temp upload BEFORE storing, so a
// blocked upload never leaves an orphaned file on disk (media:gc only
// reaps files that have a Media row).
$hash = \hash_file('sha256', $photo->getRealPath());
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$mime = $photo->getMimeType();
$storagePath = MediaPathService::get($user, 2);
$path = $photo->storePublicly($storagePath);
$hash = \hash_file('sha256', $photo);
$mime = $photo->getMimeType();
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media;
$media->status_id = null;

@ -475,11 +475,14 @@ class DirectMessageController extends Controller
abort(403, 'Invalid or unsupported mime type.');
}
// Check the blocklist against the temp upload BEFORE storing, so a
// blocked upload never leaves an orphaned file on disk (media:gc only
// reaps files that have a Media row).
$hash = \hash_file('sha256', $photo->getRealPath());
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$storagePath = MediaPathService::get($user, 2).Str::random(8);
$path = $photo->storePublicly($storagePath);
$hash = \hash_file('sha256', $photo);
abort_if(MediaBlocklistService::exists($hash) == true, 451);
$status = new Status;
$status->profile_id = $profile->id;

@ -0,0 +1,58 @@
<?php
use App\Models\User;
use App\Services\MediaBlocklistService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| Media blocklist ordering (check before store)
|--------------------------------------------------------------------------
|
| A blocklisted upload must be rejected BEFORE the file is written to storage,
| otherwise it leaves an orphaned file that media:gc (which only reaps files
| with a Media row) never cleans up.
|
*/
it('does not persist a file when the upload is blocklisted', function () {
Storage::fake(config('filesystems.default'));
$user = User::factory()->create();
$user->refresh();
// Build the upload first so we can blocklist its exact hash.
$file = UploadedFile::fake()->image('blocked.jpg', 1080, 1080);
$hash = hash_file('sha256', $file->getRealPath());
MediaBlocklistService::add($hash, ['note' => 'test']);
$this->actingAs($user)
->post('/api/compose/v0/media/upload', ['file' => $file])
->assertStatus(451);
// No media (image) file was written to the default disk. Other unrelated
// service files (e.g. cached stats json) may exist, so filter to images.
$imageFiles = collect(Storage::disk(config('filesystems.default'))->allFiles())
->filter(fn ($p) => preg_match('/\.(jpe?g|png|gif|webp)$/i', $p))
->values();
expect($imageFiles)->toBeEmpty();
});
it('accepts a non-blocklisted upload', function () {
Storage::fake(config('filesystems.default'));
$user = User::factory()->create();
$user->refresh();
$file = UploadedFile::fake()->image('ok.jpg', 1080, 1080);
$this->actingAs($user)
->post('/api/compose/v0/media/upload', ['file' => $file])
->assertOk();
});
Loading…
Cancel
Save