Files
picpeak/backend/src/utils/spreadsheetSafe.js
T
Luca cd6d57839b fix(accounting): PR #622 blockers — CSV formula injection + IMAP double-ingest race
Blocker 1 — CSV/Banana formula injection. Neither csvEscape (ledgerService) nor
the tax-report CSV escape nor the unquoted tab-separated Banana cell formatter
prefixed risky leading chars, so an admin-/sender-controlled cell beginning with
= + - @ TAB CR executes as a formula when the Treuhänder opens the export. New
shared util neutralizeSpreadsheetFormula() prepends a single quote; wired into
all three sinks (quoted CSV + unquoted Banana). Unit test pins one of each char.

Blocker 2 — IMAP intake double-ingest race. received_emails.message_id was
INDEX, not UNIQUE, and the poller ingested attachments BEFORE writing the audit
row, so a second replica / rolling-deploy overlap double-ingested the same mail.
Migration 128 makes message_id UNIQUE (nulls stay distinct); the intake now
CLAIMS the message row (status='processing') BEFORE ingesting — a concurrent
claim hits the unique constraint and skips cleanly (shared isUniqueViolation
helper). Stale 'processing' rows (worker crashed mid-ingest) are reclaimed after
10 min so no attachment is orphaned.

NOT done (deliberate): the suggested UNIQUE on inbound_documents.file_sha256 —
that column is a SOFT dedup key by design (manual re-uploads are kept as flagged
'duplicate' rows + duplicate_of_id for the Duplikat disposition); a unique index
would break that feature. The file race only yields an extra 'unsorted' row (a
data-quality nit, caught by the existing manual Duplikat backstop), not a
double-count. Rationale to be added to the PR reply.
2026-06-16 18:21:02 +02:00

20 lines
901 B
JavaScript

/**
* Formula-injection defence for spreadsheet / accounting exports (CSV + Banana).
*
* A cell whose first character is one of `= + - @ TAB CR` is evaluated as a
* formula when the file is opened in Excel / Numbers / Banana. RFC-4180
* quote-wrapping does NOT stop that evaluation — only prefixing a single quote
* does. Vectors in picpeak are real: supplier_name, invoice_number,
* payment_reference and description are admin-editable (and sender-controlled
* once incoming-mail ingestion is live).
*
* Apply to BOTH the quoted CSV and the unquoted tab-separated Banana export —
* the tab export has no surrounding quotes, so it's the more exposed of the two.
*/
function neutralizeSpreadsheetFormula(value) {
const s = value === null || value === undefined ? '' : String(value);
return /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
}
module.exports = { neutralizeSpreadsheetFormula };