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.
118 lines
4.1 KiB
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'));
|
||
|
|
}
|
||
|
|
}
|