Problem
validateFormula (js/model.js:184-194) currently falls through to new Function() when math.js cannot parse an expression. This means (()=>{while(true){}})() passes validation — and would hang the browser tab if executed through evalFormula's legacy fallback path. This is a DoS/injection vector reachable via shared links.
Plan (Phase 1.1 of adversarial-review hardening)
Step 1 — Replace validateFormula body
- If math.js available:
math.parse(expr.trim()) → return true on success, false on failure. No fallthrough to legacy new Function().
- If math.js NOT available: return false.
- Remove the legacy
new Function() call and its eslint-disable comment entirely.
Step 2 — Add expression length limit
- Reject expressions > 2000 characters (40x the longest formula in the codebase). Return false with
console.warn. Guards against pathological input before it reaches math.js's parser.
Step 3 — Fix the one demo using legacy JS syntax
js/app-demos.js:50 — Predator & Prey breed rate formula:
// Before
n.formula = 'Math.round(breedRate * prey * (1 - prey/carrying))';
// After
n.formula = 'round(breedRate * prey * (1 - prey/carrying))';
Step 4 — Update tests in test/run.js
test/run.js:2418-2423 — rename test, fix assertions:
test('validateFormula accepts math.js syntax, rejects dangerous patterns and garbage', () => {
assert(validateFormula('round(x * 2)'), 'math.js syntax valid');
assert(validateFormula('Math.round(x * 2)'), 'Math.round accepted by math.js parser');
assert(!validateFormula('2 +* )'), 'garbage rejected');
assert(!validateFormula(''), 'empty rejected');
assert(!validateFormula('(()=>{})()'), 'arrow function rejected by math.js');
assert(!validateFormula('(function(){ while(1){} })()'), 'loop rejected by math.js');
});
Step 5 — Verify
npm install # ensure mathjs available
npm test # all 229 tests pass (current 2 failures resolved)
npm run smoke # Playwright browser smoke test
What this does NOT touch
evalFormula's legacy new Function() fallback (model.js:157-179) — that's item 1.2 (removing or sandboxing the legacy evaluator).
- Items like
Math.round(), alert(), fetch() still pass math.parse at the syntax level and will be handled by 1.2's removal of the legacy evaluator.
Acknowledged regression
- CLI
--assert without npm install now fails for all expressions (previously simple comparisons passed validation via legacy new Function). This makes the mathjs dependency explicit. Non-trivial assertions already required mathjs for evaluation.
Effort
~30 minutes. 5 files touched, surgical changes only.
Problem
validateFormula(js/model.js:184-194) currently falls through tonew Function()when math.js cannot parse an expression. This means(()=>{while(true){}})()passes validation — and would hang the browser tab if executed throughevalFormula's legacy fallback path. This is a DoS/injection vector reachable via shared links.Plan (Phase 1.1 of adversarial-review hardening)
Step 1 — Replace
validateFormulabodymath.parse(expr.trim())→ return true on success, false on failure. No fallthrough to legacynew Function().new Function()call and itseslint-disablecomment entirely.Step 2 — Add expression length limit
console.warn. Guards against pathological input before it reaches math.js's parser.Step 3 — Fix the one demo using legacy JS syntax
js/app-demos.js:50— Predator & Prey breed rate formula:Step 4 — Update tests in
test/run.jstest/run.js:2418-2423— rename test, fix assertions:Step 5 — Verify
What this does NOT touch
evalFormula's legacynew Function()fallback (model.js:157-179) — that's item 1.2 (removing or sandboxing the legacy evaluator).Math.round(),alert(),fetch()still passmath.parseat the syntax level and will be handled by 1.2's removal of the legacy evaluator.Acknowledged regression
--assertwithoutnpm installnow fails for all expressions (previously simple comparisons passed validation via legacynew Function). This makes the mathjs dependency explicit. Non-trivial assertions already required mathjs for evaluation.Effort
~30 minutes. 5 files touched, surgical changes only.