modbox
Securely box untrusted JS modules, with CPU and memory limiting
modbox lets you run untrusted JavaScript by enclosing modules in a "box". It was made with JS smart contracts in mind, but can also be used anywhere you want to securely run someone else's code.
WARNING: This module is experimental and should not yet be used in production!
Features:
- CPU usage limiting ("gas metering" in Ethereum terminology)
- Memory limiting
- Modules are only allowed to access the data or APIs you explicitly give them
- Fully deterministic - code will always execute the same way no matter what
- Runs in-process - boxes are cheap to create
- Interact with boxed modules as you would any other module (e.g. call its exported functions)
- Powered by @Agoric's SES (Secure EcmaScript)
Usage
npm install modbox
let modbox = let box = console // 0boxconsole // 1boxn++ // throws - box's state is read-only
let box = modbox(code, [opts])
Creates a boxed module by evaluating the code string passed in as the code
argument. The code should export a module by setting it as module.exports
.
opts
can be an options object containing:
allowConsole
(default: false) - Set totrue
to exposeconsole.log
inside the box.globals
(default: {}) - An object which contains global variables that will be accessible from inside the box.computeLimit
(default: 10,000,000) - A number representing the amount of compute units to allow the boxed module to use. This is a rough unit, see the CPU and Memory Limit Caveats section for more info.memoryLimit
(default: 10,000,000) - A number representing the amount of memory units to allow the boxed module to use. This is a rough unit (it is NOT measured in bytes), see the CPU and Memory Limit Caveats section for more info.onBurn(value)
- A function which gets called continuously as the module executes code (both when initially created, and when its exported functions get called).value
will be the result of each JS expression (e.g. for5 + 5
,onBurn
will be called 3 times - twice withvalue = 5
, and once withvalue = 10
). If this throws, the module will be terminated and further execution will be prevented.
Execution Caveats
To prevent DoS attacks and non-determinism, this module is pretty conservative about what features are accessible from inside the box. The following are currently not allowed:
require
- instead, pass in other modules through theglobals
optionJSON
(e.g.JSON.stringify(...)
)- Regular expressions
Buffer
Date.now()
(returnsNaN
)new Date()
(returnsnew Date(NaN)
, still works if you pass in a timestamp)setTimeout()
/setInterval()
/setImmediate()
Math.random()
(throws)eval()
(not defined)- The
Function
constructor - Typed arrays (
Uint8Array
, etc.)
Some of these, e.g. Buffer
and a limited form of require
will likely be enabled in the future.
CPU and Memory Limit Caveats
CPU
CPU usage limiting works by counting up as the code executes (both in the initial eval that happens when the module is created, and when the module's functions get called from outside the box), then halting all execution once the limit is reached. This is the same as "gas" in the Ethereum world.
The metering is done by transpiling the module's source code to add calls to a "burn handler" function for every expression. The resulting code is functionally equivalent, but now calls out to this burn handler as it executes in order to measure CPU resource and memory usage.
Example:
Original code:
moduleexports = n: 0 { thisn += 1 }
Instrumented output:
;
In the resulting code, the initial module creation burns a small amount of gas, then each subsequent call to increment()
burns some more.
(In the real code, the name of burn
is actually a random string to prevent shadowing or name conflicts).
Memory
Memory usage measurement is rough and errs on the side of counting too high in order to minimize memory exhaustion attacks. Also, it counts up monotonically so usage is counted even after it is freed by garbage collection (this is because the module is intended for use with smart contracts where the box is created, run, and thrown away too quickly for GC to kick in).
Since we don't know what optimizations the JS engine is doing, we simply count from a high-level, e.g. arrays and strings are counted based on their length, objects are counted based on the number of keys, etc.
This will likely improve over time, but our targeted smart-contract use case usually doesn't require a lot of memory and the goal is simply to prevent crashing the JS engine by exhausting its memory.