diff options
| author | Thomas <thomas.giudici@proton.me> | 2026-06-18 12:17:18 +0200 |
|---|---|---|
| committer | Thomas <thomas.giudici@proton.me> | 2026-06-18 16:29:34 +0200 |
| commit | 0fbb1716fba3847a7d6f60287d20c1d9f03489cf (patch) | |
| tree | c4ac0b7ccba01138bbb1fd0d1d30041965647ff8 /src | |
| parent | a003cb75b6416488d0a448198e6de03211ae1a67 (diff) | |
| download | monzero-gui-0fbb1716fba3847a7d6f60287d20c1d9f03489cf.tar.gz monzero-gui-0fbb1716fba3847a7d6f60287d20c1d9f03489cf.tar.xz monzero-gui-0fbb1716fba3847a7d6f60287d20c1d9f03489cf.zip | |
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 <selsta@sent.at>
Diffstat (limited to 'src')
| -rw-r--r-- | src/libwalletqt/TransactionHistory.cpp | 31 |
1 files 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 <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(); |
