fix(export): neutralize CSV/DDE formula injection in exported text

Exported journal notes and scraped event titles could begin with a formula
trigger (= + - @, tab, CR) that Excel/LibreOffice execute on open.
Csv.NeutralizeFormula apostrophe-prefixes such cells so they render as text;
applied to user notes, raw event ids and scraped titles. Numeric/date cells
the exporter formats itself stay numeric for downstream analysis.
This commit is contained in:
2026-05-29 13:57:18 +03:00
parent 88615a95e9
commit 08486667c3
4 changed files with 70 additions and 4 deletions
@@ -60,7 +60,7 @@ public sealed class ExportToCsvUseCase
{
b.PlacedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture),
Title(titles, b.EventId),
b.EventId.Value,
Csv.NeutralizeFormula(b.EventId.Value),
b.Selection.Type.ToString(),
b.Selection.Side.ToString(),
b.Selection.Value?.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
@@ -68,7 +68,7 @@ public sealed class ExportToCsvUseCase
b.Stake.ToString(CultureInfo.InvariantCulture),
b.Outcome.ToString(),
b.NetProfit?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
b.Notes ?? string.Empty,
Csv.NeutralizeFormula(b.Notes),
});
return await WriteAsync("journal", Csv.Document(header, rows), ct).ConfigureAwait(false);
@@ -93,7 +93,7 @@ public sealed class ExportToCsvUseCase
{
b.OpenedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture),
Title(titles, b.EventId),
b.EventId.Value,
Csv.NeutralizeFormula(b.EventId.Value),
b.PickedSide.ToString(),
b.Rate.ToString(CultureInfo.InvariantCulture),
b.Stake.ToString(CultureInfo.InvariantCulture),
@@ -127,6 +127,8 @@ public sealed class ExportToCsvUseCase
return titles;
}
// Titles are scraped ("Side1 vs Side2") so they're treated as untrusted text and
// neutralized against CSV/formula injection.
private static string Title(IReadOnlyDictionary<DomainEventId, string> titles, DomainEventId id) =>
titles.TryGetValue(id, out var t) ? t : id.Value;
Csv.NeutralizeFormula(titles.TryGetValue(id, out var t) ? t : id.Value);
}