Upload beautify
This commit is contained in:
212
tests/Feature/Uploads/ArchiveUploadSecurityTest.php
Normal file
212
tests/Feature/Uploads/ArchiveUploadSecurityTest.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/** @var array<int, string> */
|
||||
$tempArchives = [];
|
||||
|
||||
afterEach(function () use (&$tempArchives): void {
|
||||
foreach ($tempArchives as $path) {
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
$tempArchives = [];
|
||||
});
|
||||
|
||||
/**
|
||||
* @param array<string, string> $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',
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user