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.
20 lines
901 B
JavaScript
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 };
|