TransactionHistory: prevent CSV formula injection in writeCSV
What changed, and why it matters
This update fixes a security flaw in the Monero GUI wallet's export feature. When users exported their transaction history to a CSV file, the wallet stripped quote marks from transaction notes and address labels but did nothing else. If an attacker tricked a user into sending a payment with a specially crafted note beginning with =, +, -, or @, that note could become a spreadsheet formula. When the victim later exported and opened the CSV in Excel or similar software, the formula could run, potentially stealing data or running commands. The fix prefixes risky fields with a single quote so spreadsheets treat them as plain text.
Apply the patch and ensure any downstream CSV export logic in the Monero GUI uses the same sanitization. Users who have exported CSVs from prior versions should open them only after reviewing cells for formula payloads, or import them with data-only options. Consider adding automated tests for CSV export sanitization.
Security signals we found
CSV formula injection (DDE/formula payload) in exported transaction history
Attacker-controlled input stored as transaction note from payment request tx_description
Spreadsheet formula metacharacters =, +, -, @ not neutralized by prior quote-stripping
Patch adds input sanitization helper specifically for CSV export fields
Commit message explicitly describes security relevance and attack scenario
Evidence from the diff
TransactionHistory::writeCSV() in src/libwalletqt/TransactionHistory.cpp previously removed only double-quote characters from info.label() and info.description() before writing them to CSV. Because CSV quoting does not neutralize formula metacharacters, a cell beginning with =, +, -, @, whitespace, or a control character could be interpreted as a formula by spreadsheet applications. The tx_description from a payment request is stored as the transaction note, making the field attacker-influenceable. The patch introduces a sanitizeCSVField helper that removes double quotes and prepends a single quote to any field whose first character matches those formula/control/whitespace triggers, forcing text interpretation.
Changed components
src/libwalletqt/TransactionHistory.cppTransactionHistory::writeCSV()TransactionInfo::label()TransactionInfo::description()Inspect captured patch +27 / −4
diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp
index 764a317..f5c3b75 100644
--- a/src/libwalletqt/TransactionHistory.cpp
+++ b/src/libwalletqt/TransactionHistory.cpp
@@ -36,6 +36,31 @@
#include <QWriteLocker>
#include <QtGlobal>
+namespace {
+ QString sanitizeCSVField(QString field)
+ {
+ field.remove(QChar('"'));
+
+ if (field.isEmpty()) {
+ return field;
+ }
+
+ const QChar first = field.at(0);
+ const bool needsPrefix =
+ first == QChar('=') ||
+ first == QChar('+') ||
+ first == QChar('-') ||
+ first == QChar('@') ||
+ first.isSpace() ||
+ first.category() == QChar::Other_Control;
+
+ if (needsPrefix) {
+ field.prepend(QChar('\''));
+ }
+
+ return field;
+ }
+}
bool TransactionHistory::transaction(int index, std::function<void (TransactionInfo &)> callback)
{
@@ -196,10 +221,8 @@ QString TransactionHistory::writeCSV(quint32 accountIndex, QString out)
else {
continue; // skip TransactionInfo::Direction_Both
}
- QString label = info.label();
- label.remove(QChar('"')); // reserved
- QString description = info.description();
- description.remove(QChar('"')); // reserved
+ QString label = sanitizeCSVField(info.label());
+ QString description = sanitizeCSVField(info.description());
quint64 blockHeight = info.blockHeight();
QDateTime timeStamp = info.timestamp();
QString date = info.date() + " " + info.time();
Why this scored 70/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.