From 0fbb1716fba3847a7d6f60287d20c1d9f03489cf Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 18 Jun 2026 12:17:18 +0200 Subject: TransactionHistory: prevent CSV formula injection in writeCSV writeCSV wrote the transaction note and subaddress label into the CSV stripping only the quote character. A cell beginning with =, +, - or @ can be interpreted as a formula by spreadsheet software on open, which CSV quoting does not prevent. The transaction note can be attacker-controlled: a payment request's tx_description is stored as the note when the payment is sent, so a crafted note can run a spreadsheet formula when the user later exports and opens their history, potentially enabling data exfiltration or command execution. Prefix affected fields with a single quote so they are treated as text; fields beginning with whitespace or a control character are prefixed too. Co-authored-by: selsta --- src/libwalletqt/TransactionHistory.cpp | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index 764a3178..f5c3b752 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -36,6 +36,31 @@ #include #include +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 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(); -- cgit v1.2.3