3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/tests/Feature/Reporting/AcceptanceReportCsvFormulaEscapeTest.php
2026-08-02 11:55:01 +01:00

82 lines
3.1 KiB
PHP

<?php
namespace Tests\Feature\Reporting;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\User;
use Tests\TestCase;
/**
* Regression coverage for the CSV formula injection reported by Arpit Jain
* (arpitjain099) on 2026-08-02. postAssetAcceptanceReport built its CSV
* by hand and str_replace(',', '', ...) each cell before joining with
* implode(','). Stripping commas kept the manual join from breaking but
* did nothing about formulas. Every other CSV export in
* ReportsController used League\Csv\EscapeFormula gated on
* config('app.escape_formulas'); this one did not.
*
* The fix adds the same EscapeFormula pass in the loop with the same
* gating, so a low-privilege user who plants a formula in one of the
* asset / company / user free-text fields no longer sees the payload
* execute when a reports.view user opens the CSV in Excel / LibreOffice
* / Google Sheets.
*/
class AcceptanceReportCsvFormulaEscapeTest extends TestCase
{
private function seedPendingAcceptanceWithAssetNamed(string $assetName): CheckoutAcceptance
{
// Company + asset are wired so the resulting row surfaces the
// poisoned name in the report's Name column.
$company = Company::factory()->create();
$asset = Asset::factory()->create(['name' => $assetName, 'company_id' => $company->id]);
return CheckoutAcceptance::factory()->pending()->for($asset, 'checkoutable')->create();
}
public function test_data_rows_with_formula_prefix_are_escaped_by_default()
{
$this->seedPendingAcceptanceWithAssetNamed('=HYPERLINK("http://attacker.test","click")');
$body = $this->actingAs(User::factory()->superuser()->create())
->post(route('reports/export/unaccepted_assets'))
->assertOk()
->getContent();
$this->assertStringNotContainsString('=HYPERLINK("http://attacker.test","click")', $body);
$this->assertStringContainsString('`=HYPERLINK', $body);
}
public function test_data_rows_with_plus_and_at_prefixes_are_escaped()
{
$this->seedPendingAcceptanceWithAssetNamed('+cmd|/c calc');
$this->seedPendingAcceptanceWithAssetNamed('@SUM(A1:A9)');
$body = $this->actingAs(User::factory()->superuser()->create())
->post(route('reports/export/unaccepted_assets'))
->assertOk()
->getContent();
$this->assertStringContainsString('`+cmd|', $body);
$this->assertStringContainsString('`@SUM(', $body);
}
public function test_data_rows_are_not_escaped_when_setting_disabled()
{
// Matches how the sibling exports in ReportsController behave when
// operators intentionally disable escaping.
config(['app.escape_formulas' => false]);
$this->seedPendingAcceptanceWithAssetNamed('=SUM(A1:A9)');
$body = $this->actingAs(User::factory()->superuser()->create())
->post(route('reports/export/unaccepted_assets'))
->assertOk()
->getContent();
$this->assertStringContainsString('=SUM(A1:A9)', $body);
$this->assertStringNotContainsString('`=SUM(A1:A9)', $body);
}
}