You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
szkp-map-service/tests/Unit/SecurePublicImageUploadTest...

118 lines
4.1 KiB

2 days ago
<?php
namespace Tests\Unit;
use App\Support\SecurePrivateImportUpload;
use App\Support\SecurePublicImageUpload;
use App\Support\UploadFilenameGuard;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
class SecurePublicImageUploadTest extends TestCase
{
public function test_whitelist_images_store_as_uuid_plus_safe_extension(): void
{
Storage::fake('public');
$names = ['cover.jpg', 'cover.jpeg', 'cover.png'];
if (function_exists('imagewebp')) {
$names[] = 'cover.webp';
}
foreach ($names as $name) {
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$file = UploadedFile::fake()->image($name, 12, 12);
$stored = SecurePublicImageUpload::store($file, 'uploads', 1024);
$this->assertMatchesRegularExpression('#^uploads/[0-9a-f-]{36}\.'.$ext.'$#', $stored['path']);
$this->assertContains($ext, SecurePublicImageUpload::ALLOWED_EXTENSIONS);
Storage::disk('public')->assertExists($stored['path']);
}
}
/**
* @dataProvider rejectedClientNamesProvider
*/
public function test_rejected_names_do_not_write_public_files(string $clientName): void
{
Storage::fake('public');
$file = UploadedFile::fake()->create($clientName, 8, 'text/plain');
try {
SecurePublicImageUpload::store($file, 'uploads', 1024);
$this->fail('expected validation to reject '.$clientName);
} catch (ValidationException $e) {
$this->assertArrayHasKey('file', $e->errors());
}
$this->assertSame([], Storage::disk('public')->allFiles('uploads'));
}
/**
* @return array<string, array{0: string}>
*/
public static function rejectedClientNamesProvider(): array
{
return [
'php' => ['note.php'],
'phtml' => ['note.phtml'],
'phar' => ['note.phar'],
'php_jpg' => ['1.php.jpg'],
'php_png' => ['xxx.php.png'],
];
}
public function test_jpg_name_with_non_image_bytes_is_rejected(): void
{
Storage::fake('public');
$file = UploadedFile::fake()->create('cover.jpg', 16, 'text/plain');
$this->expectException(ValidationException::class);
SecurePublicImageUpload::store($file, 'uploads', 1024);
}
public function test_filename_guard_rejects_null_byte_and_denied_segments(): void
{
$this->expectException(ValidationException::class);
UploadFilenameGuard::lastAllowedExtension("cover.php\0.jpg", SecurePublicImageUpload::ALLOWED_EXTENSIONS);
}
public function test_private_import_stays_off_public_disk(): void
{
Storage::fake('local');
Storage::fake('public');
$file = UploadedFile::fake()->create('sheet.xlsx', 12, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$absolute = SecurePrivateImportUpload::storeTemporary($file, 'tmp/declaration-import', ['xlsx', 'xls'], 10240);
$this->assertStringContainsString('/storage/app/tmp/declaration-import/', str_replace('\\', '/', $absolute));
$this->assertSame([], Storage::disk('public')->allFiles());
$relative = 'tmp/declaration-import/'.basename($absolute);
Storage::disk('local')->assertExists($relative);
Storage::disk('local')->delete($relative);
}
public function test_private_import_rejects_denied_extension(): void
{
Storage::fake('local');
Storage::fake('public');
$file = UploadedFile::fake()->create('sheet.php', 8, 'text/plain');
try {
SecurePrivateImportUpload::storeTemporary($file, 'tmp/declaration-import', ['xlsx', 'xls', 'doc', 'docx'], 10240);
$this->fail('expected validation to reject denied import extension');
} catch (ValidationException $e) {
$this->assertArrayHasKey('file', $e->errors());
}
$this->assertSame([], Storage::disk('public')->allFiles());
$this->assertSame([], Storage::disk('local')->allFiles('tmp/declaration-import'));
}
}