2ed4e30760
F-CSV: ячейки экспорта писались «как есть» — значение вроде =HYPERLINK(...) или @SUM(...) в комментарии/контакте/городе при открытии файла в Excel/ LibreOffice исполнялось как формула (OWASP Formula Injection). Новый App\Support\CsvFormulaGuard::neutralize() префиксует апострофом ячейку, начинающуюся с = + - @ TAB CR. Применён к свободному тексту в: - DealExportController (телефон/источник/город/статус/комментарий) - DealsExportProvider (телефон/контакт/статус/проект/менеджер) Числовые колонки (id/суммы/даты) не трогаются — ведущий «-» там легитимен. TenantChargesController НЕ уязвим: колонки enum(CHECK)/число/дата, свободного текста нет. TDD: 13 unit-тестов хелпера + feature-тест экспорта сделок + provider-тест. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.4 KiB
PHP
63 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Project;
|
|
use App\Models\ReportJob;
|
|
use App\Models\Tenant;
|
|
use App\Services\Reports\Providers\DealsExportProvider;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
$this->project = Project::factory()->create(['tenant_id' => $this->tenant->id, 'name' => 'Окна Москва']);
|
|
});
|
|
|
|
/** ReportJob без сохранения — провайдер читает только tenant_id + parameters. */
|
|
function dealsExportJob(int $tenantId): ReportJob
|
|
{
|
|
return new ReportJob([
|
|
'tenant_id' => $tenantId,
|
|
'type' => 'deals_export',
|
|
'parameters' => [
|
|
'format' => 'csv',
|
|
'date_from' => Carbon::now()->startOfMonth()->toDateString(),
|
|
'date_to' => Carbon::now()->endOfMonth()->toDateString(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
test('headers: 8 колонок', function () {
|
|
expect((new DealsExportProvider)->headers())
|
|
->toBe(['ID', 'Телефон', 'Контакт', 'Статус', 'Проект', 'Менеджер', 'Стоимость (₽)', 'Получено']);
|
|
});
|
|
|
|
test('нейтрализует CSV-формулы в свободном тексте (F-CSV)', function () {
|
|
DB::table('deals')->insert([
|
|
'tenant_id' => $this->tenant->id,
|
|
'project_id' => $this->project->id,
|
|
'phone' => '+79991234567',
|
|
'contact_name' => '=HYPERLINK("http://evil")',
|
|
'status' => 'new',
|
|
'received_at' => Carbon::now()->startOfMonth()->addDays(5),
|
|
'created_at' => Carbon::now(),
|
|
'updated_at' => Carbon::now(),
|
|
]);
|
|
// Имя проекта (tenant-контролируемый текст) с формульным префиксом.
|
|
DB::table('projects')->where('id', $this->project->id)->update(['name' => '@SUM(1+1)']);
|
|
|
|
$rows = (new DealsExportProvider)->rows(dealsExportJob($this->tenant->id));
|
|
|
|
expect($rows)->toHaveCount(1);
|
|
// Контакт (idx 2) и телефон (idx 1) и проект (idx 4) — нейтрализованы.
|
|
expect($rows[0][2])->toBe('\'=HYPERLINK("http://evil")');
|
|
expect($rows[0][1])->toBe("'+79991234567");
|
|
expect($rows[0][4])->toBe("'@SUM(1+1)");
|
|
// ID (idx 0) — числовой, не тронут.
|
|
expect($rows[0][0])->toBeInt();
|
|
});
|