diff --git a/js/excel.js b/js/excel.js index d93c2b0..e2fd4ad 100644 --- a/js/excel.js +++ b/js/excel.js @@ -8,6 +8,14 @@ export function readEmployeeNames(arrayBuffer) { .filter(name => name.length > 0); } +// Neutralize spreadsheet formula injection: Excel/Sheets treat a cell starting +// with = + - @ (or certain control chars) as a formula. Prefix such values with +// an apostrophe so they render as literal text. +export function sanitizeCell(value) { + const s = String(value ?? ''); + return /^[=+\-@\t\r]/.test(s) ? "'" + s : s; +} + // Pure helper: returns { names: [...], dates: [...] } columns for a schedule. // Multiple employees per date are joined alphabetically with " & ". export function buildScheduleColumns(assignments, employees) { @@ -19,7 +27,7 @@ export function buildScheduleColumns(assignments, employees) { } const sortedDates = [...byDate.keys()].sort(); const names = sortedDates.map(d => - byDate.get(d).slice().sort((x, y) => x.localeCompare(y, 'de')).join(' & ') + sanitizeCell(byDate.get(d).slice().sort((x, y) => x.localeCompare(y, 'de')).join(' & ')) ); const dates = sortedDates.map(d => { const [y, m, day] = d.split('-'); diff --git a/tests/excel.test.mjs b/tests/excel.test.mjs index 01c5806..8b7e0b2 100644 --- a/tests/excel.test.mjs +++ b/tests/excel.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { buildScheduleColumns } from '../js/excel.js'; +import { buildScheduleColumns, sanitizeCell } from '../js/excel.js'; const employees = [ { id: 'a1', name: 'Anna Müller' }, @@ -64,4 +64,22 @@ const employees = [ assert.equal(cols.names[0], ' & Anna Müller', 'unknown employee → empty string in join'); } +// sanitizeCell: formula-triggering leading chars get an apostrophe prefix +{ + assert.equal(sanitizeCell('=1+1'), "'=1+1", 'leading = neutralized'); + assert.equal(sanitizeCell('+cmd'), "'+cmd", 'leading + neutralized'); + assert.equal(sanitizeCell('-2'), "'-2", 'leading - neutralized'); + assert.equal(sanitizeCell('@x'), "'@x", 'leading @ neutralized'); + assert.equal(sanitizeCell('Anna Müller'), 'Anna Müller', 'normal name unchanged'); +} + +// buildScheduleColumns: a malicious name is sanitized in the output cell +{ + const cols = buildScheduleColumns( + [{ date: '2026-04-01', employeeId: 'x' }], + [{ id: 'x', name: '=HYPERLINK("http://evil")' }] + ); + assert.ok(cols.names[0].startsWith("'="), 'formula name prefixed with apostrophe'); +} + console.log('All excel tests passed.');