Skip to content

Harden validateFormula — remove legacy new Function() fallback #41

Description

@zntznt

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions