fix(excel): neutralize formula injection in exported names (#7)
Adds sanitizeCell(): cell values starting with = + - @ or a control char are prefixed with an apostrophe so spreadsheets render them as literal text instead of executing them as formulas. Applied to the names column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f8efcf136a
commit
e6452fa102
2 files changed
+28
-2
No files matched your search
+9
-1
@@ -8,6 +8,14 @@ export function readEmployeeNames(arrayBuffer) {
|
|||||||
.filter(name => name.length > 0);
|
.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.
|
// Pure helper: returns { names: [...], dates: [...] } columns for a schedule.
|
||||||
// Multiple employees per date are joined alphabetically with " & ".
|
// Multiple employees per date are joined alphabetically with " & ".
|
||||||
export function buildScheduleColumns(assignments, employees) {
|
export function buildScheduleColumns(assignments, employees) {
|
||||||
@@ -19,7 +27,7 @@ export function buildScheduleColumns(assignments, employees) {
|
|||||||
}
|
}
|
||||||
const sortedDates = [...byDate.keys()].sort();
|
const sortedDates = [...byDate.keys()].sort();
|
||||||
const names = sortedDates.map(d =>
|
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 dates = sortedDates.map(d => {
|
||||||
const [y, m, day] = d.split('-');
|
const [y, m, day] = d.split('-');
|
||||||
|
|||||||
+19
-1
@@ -1,5 +1,5 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { buildScheduleColumns } from '../js/excel.js';
|
import { buildScheduleColumns, sanitizeCell } from '../js/excel.js';
|
||||||
|
|
||||||
const employees = [
|
const employees = [
|
||||||
{ id: 'a1', name: 'Anna Müller' },
|
{ 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');
|
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.');
|
console.log('All excel tests passed.');
|
||||||
Reference in new issue
Block a user