Skip to content

CWE-95: Eval Injection - JavaScript

Overview

In JavaScript/TypeScript applications, CWE-95 vulnerabilities occur when untrusted input is passed to dynamic code execution functions like eval(), Function(), setTimeout()/setInterval() with string arguments, new Function(), or module loading mechanisms like require() in Node.js. Untrusted input can originate from HTTP requests, external APIs, databases, files, message queues, or any source outside the application's control. JavaScript's highly dynamic nature and ubiquitous use of eval-like constructs make it particularly susceptible to code injection attacks.

Primary Defence: Never use eval() or Function() constructor with user input; use safe alternatives like JSON.parse() for data deserialization, implement math expression parsers (like mathjs or expr-eval) instead of eval for calculations, use static imports or controlled dynamic imports with allowlists, enable Content Security Policy (CSP) with 'unsafe-eval' disabled to prevent eval execution, and validate all input against strict patterns to prevent arbitrary code execution.

JavaScript applications are vulnerable when evaluating untrusted input for calculations or business logic, using eval() for JSON parsing (deprecated), dynamically loading modules based on untrusted input, using template literals without proper escaping, evaluating code in vm module without proper sandboxing, or using unsafe deserialization in Node.js. Both browser and server-side JavaScript environments can be compromised through eval injection.

Common scenarios include: calculator applications using eval() for expression evaluation, configuration systems that evaluate JavaScript code, dynamic module loading in Node.js applications, server-side rendering with unsafe template evaluation, and WebSocket or API endpoints that execute untrusted code. Modern frameworks like React, Vue, and Angular provide built-in protections, but developers can still introduce vulnerabilities through direct use of eval-like constructs.

This guidance demonstrates how to eliminate eval injection in JavaScript/TypeScript by replacing eval() with safe expression parsers, using JSON for data serialization, implementing controlled module loading, and employing Content Security Policy (CSP) to prevent eval execution.

Common Vulnerable Patterns

Direct eval() on Untrusted Input

// VULNERABLE - Direct evaluation of untrusted input
const express = require('express');
const app = express();
app.use(express.json());

app.post('/calculate', (req, res) => {
  const expression = req.body.expression;

  // CRITICAL VULNERABILITY - eval executes arbitrary code
  const result = eval(expression);

  res.json({ result });
});

// Attack examples:
// {"expression": "require('child_process').execSync('whoami').toString()"}
// {"expression": "require('fs').readFileSync('/etc/passwd', 'utf8')"}
// {"expression": "process.exit(0)"}  // Crashes server
// {"expression": "global.SECRET_KEY"}  // Steals secrets
// All execute with full Node.js privileges!

Function Constructor with Untrusted Input

// VULNERABLE - Function constructor is equivalent to eval
app.post('/run-function', (req, res) => {
  const code = req.body.code;

  // CRITICAL VULNERABILITY - Function() executes arbitrary code
  const fn = new Function('x', 'y', code);
  const result = fn(5, 3);

  res.json({ result });
});

// Attack example:
// {"code": "return require('child_process').execSync('ls -la').toString()"}
// Executes system commands!

setTimeout/setInterval with String Arguments

// VULNERABLE - String arguments to setTimeout are evaluated
app.post('/schedule-task', (req, res) => {
  const task = req.body.task;
  const delay = req.body.delay || 1000;

  // CRITICAL VULNERABILITY - setTimeout with string evaluates code
  setTimeout(task, delay);

  res.json({ scheduled: true });
});

// Attack example:
// {"task": "require('fs').unlinkSync('/important/file.txt')", "delay": 100}
// Deletes files after 100ms!

Dynamic require() in Node.js

// VULNERABLE - Dynamic module loading based on untrusted input
app.post('/load-plugin', (req, res) => {
  const pluginName = req.body.plugin;

  // CRITICAL VULNERABILITY - arbitrary module loading
  const plugin = require(pluginName);
  const result = plugin.execute();

  res.json({ result });
});

// Attack examples:
// {"plugin": "child_process"}  // Load child_process module
// {"plugin": "/etc/passwd"}  // Read system files
// {"plugin": "../../../package.json"}  // Directory traversal
// Attacker gains access to any Node.js module or file!

VM Module Without Proper Sandboxing

// VULNERABLE - vm.runInThisContext allows escaping
const vm = require('vm');

app.post('/run-script', (req, res) => {
  const script = req.body.script;

  // CRITICAL VULNERABILITY - runs in current context
  const result = vm.runInThisContext(script);

  res.json({ result });
});

// Even vm.runInNewContext can be escaped:
app.post('/run-isolated', (req, res) => {
  const script = req.body.script;

  // STILL VULNERABLE - sandbox can be escaped
  const sandbox = { result: null };
  vm.runInNewContext(script, sandbox);

  res.json({ result: sandbox.result });
});

// Attack: Escape sandbox
// {"script": "this.constructor.constructor('return process')().exit()"}
// Gains access to process object and crashes server!

Client-Side eval in React/Vue

// VULNERABLE - Client-side eval in React component
import React, { useState } from 'react';

function Calculator() {
  const [expression, setExpression] = useState('');
  const [result, setResult] = useState(null);

  const calculate = () => {
    // CRITICAL VULNERABILITY - eval in browser
    try {
      const res = eval(expression);
      setResult(res);
    } catch (e) {
      setResult('Error');
    }
  };

  return (
    <div>
      <input 
        value={expression} 
        onChange={(e) => setExpression(e.target.value)} 
      />
      <button onClick={calculate}>Calculate</button>
      <p>Result: {result}</p>
    </div>
  );
}

// Attack in browser:
// User types: document.cookie
// Steals session cookies!
// 
// Or: fetch('/api/admin', {method:'DELETE'})
// Performs unauthorized actions!

Secure Patterns

Safe Math Expression Evaluator

// SECURE - Custom parser for safe math expressions
const express = require('express');
const app = express();
app.use(express.json());

class SafeMathEvaluator {
  constructor() {
    // Allowlist of safe operators
    this.operators = {
      '+': (a, b) => a + b,
      '-': (a, b) => a - b,
      '*': (a, b) => a * b,
      '/': (a, b) => {
        if (b === 0) throw new Error('Division by zero');
        return a / b;
      },
      '^': (a, b) => {
        if (Math.abs(b) > 100) throw new Error('Exponent too large');
        return Math.pow(a, b);
      }
    };

    // Allowlist of safe functions
    this.functions = {
      'abs': Math.abs,
      'ceil': Math.ceil,
      'floor': Math.floor,
      'round': Math.round,
      'max': Math.max,
      'min': Math.min,
      'sqrt': Math.sqrt
    };
  }

  evaluate(expression) {
    // Validate expression format
    if (typeof expression !== 'string' || expression.length > 200) {
      throw new Error('Invalid expression');
    }

    // Only allow numbers, operators, parentheses, and function names
    const allowedPattern = /^[0-9+\-*/^().a-z\s]+$/i;
    if (!allowedPattern.test(expression)) {
      throw new Error('Expression contains invalid characters');
    }

    // Parse and evaluate using safe methods
    return this.parseExpression(expression);
  }

  parseExpression(expr) {
    // Remove whitespace
    expr = expr.replace(/\s/g, '');

    // Check for function calls
    const functionPattern = /([a-z]+)\(([^)]+)\)/i;
    const match = expr.match(functionPattern);

    if (match) {
      const funcName = match[1].toLowerCase();
      const args = match[2];

      if (!this.functions[funcName]) {
        throw new Error(`Unknown function: ${funcName}`);
      }

      // Parse arguments
      const argValues = args.split(',').map(arg => 
        this.parseExpression(arg.trim())
      );

      // Execute safe function
      return this.functions[funcName](...argValues);
    }

    // Parse simple arithmetic expressions
    return this.parseArithmetic(expr);
  }

  parseArithmetic(expr) {
    // Simple recursive descent parser for arithmetic
    // This is a simplified version - production code should be more robust

    // Handle numbers
    const num = parseFloat(expr);
    if (!isNaN(num)) {
      return num;
    }

    // Handle parentheses
    if (expr.startsWith('(') && expr.endsWith(')')) {
      return this.parseArithmetic(expr.slice(1, -1));
    }

    // Handle operators (simplified)
    for (const [op, fn] of Object.entries(this.operators)) {
      const index = expr.lastIndexOf(op);
      if (index > 0) {
        const left = this.parseArithmetic(expr.slice(0, index));
        const right = this.parseArithmetic(expr.slice(index + 1));
        return fn(left, right);
      }
    }

    throw new Error('Invalid expression');
  }
}

app.post('/calculate', (req, res) => {
  const expression = req.body.expression;

  try {
    const evaluator = new SafeMathEvaluator();
    const result = evaluator.evaluate(expression);
    res.json({ result });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Safe to use with:
// "2 + 2" → 4
// "abs(-5)" → 5
// "max(1, 2, 3)" → 3

// Safely rejects:
// "require('fs')" → "Invalid characters"
// "process.exit()" → "Invalid characters"

Why This Works

This pattern eliminates eval injection by implementing a custom expression evaluator that never executes arbitrary JavaScript code. Unlike eval() which has access to the entire JavaScript runtime including require(), process, and global objects, this parser only recognizes mathematical operators and a predefined set of safe functions. The operator allowlist ensures attackers cannot call dangerous functions - if an operation isn't explicitly mapped in the SAFE_OPERATORS or SAFE_FUNCTIONS objects, it simply doesn't exist in this sandboxed environment.

The character validation provides defense-in-depth by rejecting any expression containing characters outside the mathematical domain before parsing begins. This prevents injection attempts like "require('child_process')" or "process.exit()" from even reaching the evaluation logic. The parser's recursive structure safely handles operator precedence and parentheses without introducing code execution paths. Length limits prevent denial-of-service attacks through extremely long expressions that could cause resource exhaustion.

This approach offers superior security compared to eval() with restricted globals, which can still be escaped through techniques like accessing constructor chains. The custom parser has zero exposure to the JavaScript runtime - it only performs arithmetic operations on numbers. For production applications needing more mathematical capabilities, well-maintained libraries like math.js or expr-eval provide similar security guarantees with additional features like variable support and more functions, all while maintaining isolation from dangerous runtime access.

Using math.js or expr-eval Libraries

// SECURE - Use well-tested math expression libraries
const express = require('express');
const math = require('mathjs');
const app = express();
app.use(express.json());

// Configure math.js with restricted scope
const mathEvaluator = math.create({
  // Import only safe functions
  import: ['add', 'subtract', 'multiply', 'divide', 'pow', 'sqrt', 
           'abs', 'round', 'ceil', 'floor', 'max', 'min']
});

app.post('/calculate', (req, res) => {
  const expression = req.body.expression;

  // Validate input
  if (!expression || expression.length > 200) {
    return res.status(400).json({ error: 'Invalid expression' });
  }

  try {
    // math.js safely evaluates mathematical expressions
    // Does NOT execute arbitrary JavaScript code
    const result = mathEvaluator.evaluate(expression);
    res.json({ result });
  } catch (error) {
    res.status(400).json({ error: 'Invalid expression' });
  }
});

// Alternative: Using expr-eval library
const { Parser } = require('expr-eval');

app.post('/calculate-alt', (req, res) => {
  const expression = req.body.expression;

  if (!expression || expression.length > 200) {
    return res.status(400).json({ error: 'Invalid expression' });
  }

  try {
    const parser = new Parser();
    const expr = parser.parse(expression);
    const result = expr.evaluate();
    res.json({ result });
  } catch (error) {
    res.status(400).json({ error: 'Invalid expression' });
  }
});

// Safe expressions:
// "2 + 2 * 3" → 8
// "sqrt(16) + pow(2, 3)" → 12

// Safely blocks:
// "require('fs')" → Syntax error
// "global.process" → Not allowed

Why This Works

Using established mathematical expression libraries eliminates eval injection because these libraries are specifically designed to parse and evaluate mathematical expressions without executing arbitrary JavaScript code. Math.js and expr-eval maintain complete isolation from the JavaScript runtime - they cannot access require(), global objects, or any Node.js APIs. The restricted import configuration in math.js explicitly specifies which mathematical operations are available, creating a minimal attack surface. Even if an attacker tries to inject code, the parser only understands mathematical syntax, not JavaScript language constructs.

These libraries have undergone extensive security review and testing by the open-source community, with thousands of production deployments providing real-world validation. Unlike custom parsers which might have overlooked edge cases, mature libraries handle complex scenarios like operator precedence, function composition, and numeric edge cases correctly. The expr-eval library in particular uses a proper lexer and parser that strictly enforces mathematical grammar, rejecting any input that doesn't conform to mathematical notation.

For applications migrating from eval(), these libraries provide equivalent functionality for mathematical expressions while eliminating code injection risks. The performance is also excellent since they use optimized parsing and evaluation algorithms. If your use case extends beyond pure mathematics to include variables or custom functions, both libraries support those features safely through controlled APIs. Always validate input length and complexity to prevent denial-of-service, but the code injection risk is completely eliminated by the library's architectural isolation from the JavaScript runtime.

Operation Mapping for Business Logic

// SECURE - Explicit operation mapping instead of dynamic execution
const express = require('express');
const app = express();
app.use(express.json());

class SafeOperationHandler {
  constructor() {
    // Explicit allowlist of safe operations
    this.operations = {
      'add': (a, b) => Number(a) + Number(b),
      'subtract': (a, b) => Number(a) - Number(b),
      'multiply': (a, b) => Number(a) * Number(b),
      'divide': (a, b) => {
        const divisor = Number(b);
        if (divisor === 0) {
          throw new Error('Division by zero');
        }
        return Number(a) / divisor;
      },
      'power': (a, b) => {
        const exp = Number(b);
        if (Math.abs(exp) > 100) {
          throw new Error('Exponent too large');
        }
        return Math.pow(Number(a), exp);
      },
      'percentage': (value, percent) => {
        return Number(value) * (Number(percent) / 100);
      }
    };
  }

  execute(operation, ...args) {
    // Validate operation is in allowlist
    if (!this.operations.hasOwnProperty(operation)) {
      throw new Error(`Invalid operation: ${operation}`);
    }

    // Validate all arguments are numbers
    for (const arg of args) {
      if (typeof arg !== 'number' && isNaN(Number(arg))) {
        throw new Error('Invalid numeric argument');
      }
    }

    // Execute allowlisted operation
    const fn = this.operations[operation];
    return fn(...args);
  }
}

app.post('/calculate', (req, res) => {
  const { operation, a, b } = req.body;

  try {
    const handler = new SafeOperationHandler();
    const result = handler.execute(operation, a, b);
    res.json({ result });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Usage:
// POST /calculate {"operation": "add", "a": 5, "b": 3} → 8
// POST /calculate {"operation": "multiply", "a": 4, "b": 7} → 28

// Safely rejects:
// POST /calculate {"operation": "exec", ...} → "Invalid operation"

Why This Works

This pattern prevents eval injection by replacing dynamic code execution with a static mapping of operation names to predefined functions. Instead of interpreting user input as code with eval(), the system treats the operation name as a dictionary key that must match one of the explicitly registered operations. Attackers cannot inject code because the lookup mechanism only accepts string keys - if the operation isn't in the allowlist, it returns undefined, and the validation throws an error. There's no code path that interprets or executes user input as JavaScript.

The type validation layer ensures all numeric inputs are properly converted and validated before being passed to operation functions. Bounds checking on operations like power prevents denial-of-service attacks where attackers might try to compute astronomically large numbers. Each operation function is self-contained with its own validation logic (like division by zero checks), ensuring robust error handling. This design makes the code highly maintainable since adding new operations simply requires adding an entry to the operations object.

Compared to using eval() which gives attackers access to the entire JavaScript language, this approach provides exactly the functionality needed - no more, no less. The performance is superior since there's no parsing or compilation overhead, just a simple object lookup and function call. This pattern is ideal for business logic applications where users need to select from predefined operations with their own parameters. For more complex expression evaluation, combine this with math.js or the custom parser pattern, but for simple operation selection, this mapping approach is the most straightforward and secure solution.

Controlled Module Loading with Allowlist

// SECURE - Plugin system with allowlist
const express = require('express');
const path = require('path');
const app = express();
app.use(express.json());

class SafePluginLoader {
  constructor() {
    // Explicit allowlist of approved plugins
    this.allowedPlugins = {
      'dataValidator': './plugins/dataValidator',
      'reportGenerator': './plugins/reportGenerator',
      'emailSender': './plugins/emailSender'
    };

    // Cache loaded plugins
    this.pluginCache = new Map();
  }

  loadPlugin(pluginName) {
    // Validate plugin is in allowlist
    const pluginPath = this.allowedPlugins[pluginName];
    if (!pluginPath) {
      throw new Error(`Plugin not allowed: ${pluginName}`);
    }

    // Check cache
    if (this.pluginCache.has(pluginName)) {
      return this.pluginCache.get(pluginName);
    }

    // Load only the allowlisted module
    const plugin = require(pluginPath);

    // Validate plugin interface
    if (typeof plugin.execute !== 'function') {
      throw new Error('Plugin missing execute method');
    }

    // Cache and return
    this.pluginCache.set(pluginName, plugin);
    return plugin;
  }

  executePlugin(pluginName, parameters) {
    // Validate parameters are safe types
    this.validateParameters(parameters);

    // Load and execute
    const plugin = this.loadPlugin(pluginName);
    return plugin.execute(parameters);
  }

  validateParameters(params) {
    if (params === null || params === undefined) {
      return;
    }

    if (typeof params !== 'object') {
      throw new Error('Parameters must be an object');
    }

    // Check all values are safe types
    for (const [key, value] of Object.entries(params)) {
      const type = typeof value;
      if (type !== 'string' && type !== 'number' && 
          type !== 'boolean' && value !== null) {
        throw new Error(`Unsafe parameter type: ${type}`);
      }
    }
  }
}

app.post('/run-plugin', (req, res) => {
  const { pluginName, parameters } = req.body;

  try {
    const loader = new SafePluginLoader();
    const result = loader.executePlugin(pluginName, parameters);
    res.json({ result });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Usage:
// POST /run-plugin {
//   "pluginName": "dataValidator",
//   "parameters": {"data": "test"}
// }

// Safely rejects:
// POST /run-plugin {"pluginName": "child_process"} → "Plugin not allowed"
// POST /run-plugin {"pluginName": "../../../etc/passwd"} → "Plugin not allowed"

Why This Works

This pattern eliminates module loading vulnerabilities by strictly controlling which modules can be loaded through an explicit allowlist. Instead of passing untrusted user input directly to require(), the system maps user-provided plugin names to hardcoded module paths that developers have vetted. Attackers cannot load arbitrary modules like 'child_process' or 'fs' because only the plugins explicitly registered in the allowedPlugins map can be required. The module paths are trusted constants defined at development time, not runtime values from user input.

The interface validation ensures all loaded plugins conform to a known contract with an execute() method, preventing loading of modules with dangerous initialization code or unexpected side effects. Parameter validation restricts plugin inputs to safe primitive types, preventing object injection attacks where malicious objects with custom toString() or valueOf() methods might execute code. The caching mechanism prevents repeated loading that could trigger module initialization multiple times, while also improving performance.

For applications requiring extensibility, this pattern provides a secure plugin architecture without the risks of dynamic require(). The allowlist can be managed through configuration files or environment variables, allowing plugin additions without code changes while maintaining security. Compare this to dynamic require() with path traversal protections - those can often be bypassed, whereas an allowlist approach has no bypass potential since only predefined modules are accessible. This pattern works well with npm workspaces or monorepos where plugins are developed alongside the main application and referenced by name rather than path.

Safe JSON Parsing (Never eval for JSON)

// SECURE - Always use JSON.parse, never eval
const express = require('express');
const app = express();

app.post('/parse-data', express.text(), (req, res) => {
  const jsonString = req.body;

  try {
    // CORRECT - Use JSON.parse
    const data = JSON.parse(jsonString);

    // Validate structure
    if (typeof data !== 'object' || data === null) {
      return res.status(400).json({ error: 'Invalid data structure' });
    }

    res.json({ parsed: data });
  } catch (error) {
    res.status(400).json({ error: 'Invalid JSON' });
  }
});

// NEVER do this for JSON parsing:
// const data = eval('(' + jsonString + ')');  // DANGEROUS!

// JSON.parse is safe because:
// - It only parses data, never executes code
// - Cannot access JavaScript runtime
// - Throws error on invalid JSON
// - No code injection possible

Why this works:

JSON.parse() is fundamentally safe from code injection because JSON is a pure data format with no mechanism for encoding executable code. Unlike JavaScript object literals which can contain function expressions, getters, setters, and other executable constructs, JSON supports only six data types: objects, arrays, strings, numbers, booleans, and null. The JSON.parse() function is implemented in native code (C++ in Node.js/V8) and never invokes the JavaScript interpreter to execute code - it only constructs data structures.

Historically, developers sometimes used eval() to parse JSON because it could handle JavaScript object literal syntax including single quotes and unquoted keys. This was never safe, even with wrapping in parentheses, because JSON strings could contain executable code. JSON.parse() eliminates this risk entirely by strictly enforcing the JSON specification - any input containing JavaScript code is invalid JSON and triggers a parse error. The parser's strict mode rejects common injection attempts like unquoted keys, single quotes, comments, or undefined values.

For modern applications, JSON.parse() should be the only method used for parsing JSON data, regardless of whether the data comes from trusted sources. It's faster than eval() since it uses optimized native parsing, produces better error messages for debugging, and integrates seamlessly with type validation libraries. If you need to handle relaxed JSON syntax (comments, unquoted keys), use a specialized library like json5, but never eval(). For serialization, JSON.stringify() is equally safe, though be aware of circular references which throw errors by default.

Content Security Policy (CSP) for Browser Protection

// SECURE - Implement CSP to block eval in browser
const express = require('express');
const helmet = require('helmet');
const app = express();

// Configure helmet with strict CSP
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        "'self'",
        // DON'T use 'unsafe-eval' or 'unsafe-inline'
        // ✅ Use nonces or hashes for inline scripts
      ],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: [],
    },
  })
);

// Serve React/Vue app with CSP
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <title>Secure App</title>
        <!-- CSP prevents eval() from executing -->
      </head>
      <body>
        <div id="root"></div>
        <script src="/app.js"></script>
      </body>
    </html>
  `);
});

// Security mechanisms:
// - CSP blocks eval(), Function(), setTimeout with strings
// - Prevents inline script execution
// - Requires script sources to be allowlisted
// - Provides defense-in-depth

// With CSP, even if eval() is called, browser blocks it:
// eval('alert(1)') → CSP violation, blocked by browser

Why This Works

Content Security Policy provides a browser-level defense against eval injection by instructing the browser to refuse executing certain types of code regardless of what the JavaScript tries to do. When CSP is configured without 'unsafe-eval', the browser's JavaScript engine will throw an error if code attempts to call eval(), Function(), setTimeout() with strings, or setInterval() with strings. This enforcement happens at the browser level, not in application code, making it impossible for attackers to bypass even if they find a way to inject JavaScript.

CSP operates on a principle of allowlisting trusted sources for various resource types. By excluding 'unsafe-eval' from script-src directives, you tell the browser that dynamic code evaluation is never legitimate in your application. This catches even zero-day vulnerabilities in your code - if an attacker finds an injection point you weren't aware of, CSP blocks the eval() call before it can execute. The policy is enforced before any JavaScript runs, providing true defense-in-depth. Modern browsers support CSP reporting, allowing you to monitor violations and detect attempted attacks.

For single-page applications, CSP is essential because XSS vulnerabilities can lead to eval injection. Even if you've carefully avoided eval() in your own code, third-party libraries or vulnerable dependencies might introduce it. CSP acts as a safety net, blocking these dangerous operations. When implementing CSP, avoid 'unsafe-inline' and 'unsafe-eval' directives - use nonces or hashes for inline scripts instead. For legacy applications migrating to CSP, start with report-only mode to identify violations without breaking functionality, then gradually refactor incompatible code. CSP won't protect against server-side eval injection in Node.js, but it's invaluable for client-side security.

Safe Template Rendering

// SECURE - Use template engines with auto-escaping
const express = require('express');
const Handlebars = require('handlebars');
const app = express();
app.use(express.json());

// Handlebars with auto-escaping
app.post('/render-message', (req, res) => {
  const { title, content, author } = req.body;

  // Pre-defined template (NOT untrusted input!)
  const templateString = `
    <div class="message">
      <h2>{{title}}</h2>
      <p>{{content}}</p>
      <small>From: {{author}}</small>
    </div>
  `;

  // Compile template
  const template = Handlebars.compile(templateString);

  // Validate context values
  const context = {
    title: String(title || ''),
    content: String(content || ''),
    author: String(author || '')
  };

  // Render with user data (auto-escaped)
  const html = template(context);

  res.json({ html });
});

// Safe usage:
// POST /render-message {
//   "title": "Hello",
//   "content": "This is a message",
//   "author": "John"
// }

// Safely handles:
// POST /render-message {
//   "content": "<script>alert(1)</script>"
// }
// → Script is escaped as text, not executed

Why This Works

This pattern prevents eval injection by separating template structure from user data. The critical security principle is that templates come from trusted sources (developers, configuration files) while users provide only the data values to populate those templates. Handlebars and similar template engines are designed to interpolate data safely without executing arbitrary code. When you render "{{title}}", Handlebars inserts the string value of title - it doesn't evaluate it as JavaScript. This architectural separation means attackers cannot inject template directives through data values.

The auto-escaping feature provides defense-in-depth against related vulnerabilities. If a user provides data containing HTML or script tags, Handlebars converts them to HTML entities ("&lt;script&gt;" instead of "<script>"), preventing XSS attacks. Some developers mistakenly use eval() to render templates dynamically, which is extremely dangerous because it executes the entire template as code. Template engines avoid this by parsing templates into abstract syntax trees at compile time, then efficiently replacing placeholders with data at runtime without any code execution.

For applications where users need to customize templates (email templates, report formats), provide a safe template language with limited capabilities rather than exposing full Handlebars or JavaScript. Libraries like Liquid (used by Shopify) offer sandboxed template execution with controlled access to objects and operations. If you must allow user templates, use strict sandboxing with allowlists for available functions and objects, disable helpers that could access files or network, and set execution timeouts. Never concatenate strings to build templates dynamically from user input - always use the template engine's parameter binding.

React/Vue Safe Patterns

// SECURE - React calculator without eval
import React, { useState } from 'react';
import { evaluate } from 'mathjs';

function SafeCalculator() {
  const [expression, setExpression] = useState('');
  const [result, setResult] = useState(null);

  const calculate = async () => {
    try {
      // Use math.js library (safe)
      const res = evaluate(expression);
      setResult(res);
    } catch (e) {
      setResult('Invalid expression');
    }
  };

  return (
    <div>
      <input 
        type="text"
        value={expression} 
        onChange={(e) => setExpression(e.target.value)}
        placeholder="Enter math expression"
      />
      <button onClick={calculate}>Calculate</button>
      <p>Result: {result}</p>
    </div>
  );
}

// Vue.js safe pattern
export default {
  data() {
    return {
      expression: '',
      result: null,
      operations: {
        add: (a, b) => Number(a) + Number(b),
        subtract: (a, b) => Number(a) - Number(b),
        multiply: (a, b) => Number(a) * Number(b),
        divide: (a, b) => Number(a) / Number(b)
      }
    };
  },
  methods: {
    calculate() {
      // Use predefined operations, not eval
      const match = this.expression.match(/(\d+)\s*([+\-*/])\s*(\d+)/);
      if (!match) {
        this.result = 'Invalid format';
        return;
      }

      const [, a, op, b] = match;
      const opMap = { '+': 'add', '-': 'subtract', '*': 'multiply', '/': 'divide' };
      const operation = this.operations[opMap[op]];

      if (operation) {
        this.result = operation(a, b);
      } else {
        this.result = 'Invalid operation';
      }
    }
  }
};

Why This Works

Modern frontend frameworks like React and Vue are designed with security as a core principle, automatically escaping user data and preventing code injection when used correctly. React's JSX syntax ensures that any data interpolated in curly braces is treated as values, not code - even if a user provides "<script>alert(1)</script>" as input, React converts it to a text string rather than executing it. Vue has similar protections with its template syntax. The key is to never use eval(), Function(), or innerHTML with untrusted data, and instead rely on the framework's built-in data binding.

For mathematical expressions in React/Vue applications, using libraries like math.js provides the same security benefits in the browser as on the server. These libraries are compiled into your bundle and run in the browser's JavaScript engine, but they maintain strict isolation from dangerous APIs. The safe pattern demonstrates using predefined operations (add, subtract, multiply, divide) mapped to functions rather than evaluating user input as code. Input validation ensures expressions match expected patterns before processing, and CSP provides an additional browser-level defense against eval attempts.

When building calculators, form validators, or other interactive features, resist the temptation to use eval() for convenience. The predefined operations approach might require more code initially, but it's vastly more secure and maintainable. For complex scenarios needing variable support or custom functions, math.js offers these features safely through its parser. Remember that client-side security is defense-in-depth - server-side validation is still essential since clients can be manipulated. However, preventing eval injection in the browser protects against XSS-based attacks and reduces the attack surface significantly.

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Additional Resources