*/ $tempArchives = []; afterEach(function () use (&$tempArchives): void { foreach ($tempArchives as $path) { if (is_file($path)) { @unlink($path); } } $tempArchives = []; }); /** * @param array $entries * @param (callable(\ZipArchive,string):void)|null $entryCallback */ function makeArchiveUpload(array $entries, array &$tempArchives, ?callable $entryCallback = null): UploadedFile { if (! class_exists(\ZipArchive::class)) { test()->markTestSkipped('ZipArchive extension is required.'); } $path = tempnam(sys_get_temp_dir(), 'sb_upload_zip_'); if ($path === false) { throw new \RuntimeException('Failed to allocate temp archive path.'); } $tempArchives[] = $path; $zip = new \ZipArchive(); if ($zip->open($path, \ZipArchive::OVERWRITE | \ZipArchive::CREATE) !== true) { throw new \RuntimeException('Failed to create test zip archive.'); } foreach ($entries as $name => $content) { $zip->addFromString($name, $content); if ($entryCallback !== null) { $entryCallback($zip, $name); } } $zip->close(); return new UploadedFile($path, 'archive.zip', 'application/zip', null, true); } it('rejects archive with zip slip path during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $archive = makeArchiveUpload([ '../evil.txt' => 'x', ], $tempArchives); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertStatus(422) ->assertJsonPath('message', 'Archive inspection failed.'); expect(strtolower((string) $response->json('reason')))->toContain('path'); }); it('rejects archive with symlink during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $archive = makeArchiveUpload([ 'safe/readme.txt' => 'ok', 'safe/link' => 'target', ], $tempArchives, function (\ZipArchive $zip, string $entryName): void { if ($entryName === 'safe/link') { $zip->setExternalAttributesName($entryName, \ZipArchive::OPSYS_UNIX, 0120777 << 16); } }); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertStatus(422) ->assertJsonPath('message', 'Archive inspection failed.'); expect(strtolower((string) $response->json('reason')))->toContain('symlink'); }); it('rejects archive with deep nesting during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $archive = makeArchiveUpload([ 'a/b/c/d/e/f/file.txt' => 'deep', ], $tempArchives); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertStatus(422) ->assertJsonPath('message', 'Archive inspection failed.'); expect(strtolower((string) $response->json('reason')))->toContain('depth'); }); it('rejects archive with too many files during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $entries = []; for ($index = 0; $index < 5001; $index++) { $entries['f' . $index . '.txt'] = 'x'; } $archive = makeArchiveUpload($entries, $tempArchives); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertStatus(422) ->assertJsonPath('message', 'Archive inspection failed.'); expect((string) $response->json('reason'))->toContain('5000'); }); it('rejects archive with executable inside during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $archive = makeArchiveUpload([ 'safe/readme.txt' => 'ok', 'safe/run.exe' => 'MZ', ], $tempArchives); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertStatus(422) ->assertJsonPath('message', 'Archive inspection failed.'); expect(strtolower((string) $response->json('reason')))->toContain('blocked'); }); it('rejects archive with zip bomb ratio during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $archive = makeArchiveUpload([ 'payload.txt' => str_repeat('A', 6 * 1024 * 1024), ], $tempArchives); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertStatus(422) ->assertJsonPath('message', 'Archive inspection failed.'); expect(strtolower((string) $response->json('reason')))->toContain('ratio'); }); it('accepts valid archive during preload', function () use (&$tempArchives) { Storage::fake('local'); $user = User::factory()->create(); $archive = makeArchiveUpload([ 'skins/theme/readme.txt' => 'hello', 'skins/theme/layout.ini' => 'v=1', ], $tempArchives); $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $archive, 'screenshots' => [$screenshot], ]); $response->assertOk()->assertJsonStructure([ 'upload_id', 'status', 'expires_at', ]); });