Server side Rust: sandboxing untrusted user JS

Our Rust program is running server side (x86_64). It takes untrusted (possibly malicious) user-submitted JS, and tries to execute it. What is the best route for sandboxing ?

We need to defend (including but not limited to):

  • attempts at malicious system calls
  • while(1) {}
  • while(1) { malloc(..); } // JS equiv of

Currently, the best I can think of is JS -> wasm compiler + using the wasm32 sandboxing capabilities. I'm okay with limiting user scripts to 4GB RAM.

Suggestions for JS -> wasm crates welcome.
Suggestions for other sandboxing aproaches (deno based?) welcome too?

My main focuses are (in order):

  1. security: prevent breaking out
  2. kill resource hogs
  3. throughput
1 Like

With the big disclaimer that I've not implemented anything like this myself - two other approaches I've seen used for this are:

  • V8 isolates (this is what Cloudflare Workers and Deno Deploy use - there's a good article on the latter here, which includes some more detail on how they approach security)
  • Firecracker micro VMs (this is what AWS Lambda and Fly.io use)
4 Likes
  1. Thanks for mentioning these.

  2. More "out of the box" solutions like these are welcome. It's easier to filter solutions than to come up with solutions. Anything that "sandboxes" JS, I'm interested in hearing about.

2 Likes

My not-super-informed impression is that JS is pretty isolated already.

If you don't provide JS bindings to Rust functions that make syscalls, I think JS is pretty isolated from the host environment.

As far as the memory and resource limits, I think with the v8, crate, for instance, you are able to control the execution of the JS event loop, which means you can stop it in the middle of it's infinite loop if it takes too long.

You can also get the heap limits so that it can't take too much memory.

Like @17cupsofcoffee I believe isolates are used in security critical scenarios such as edge computing.

Heap limits and controlling the event loop should cover that.

Isolates can be created extremely fast.

I love the idea of firecracker, and I've done some testing with it before. I think the limitation here is that it requires a KVM hypervisor, which means running on public cloud VMs can be difficult. I think you have to run it on relatively expensive bare metal cloud instances or something with nested virtualization.

And since you only need JS execution, I think v8 isolates are probably the way to go, out of the technologies I'm aware of, anyway.

deno_core gives a very nice API for running v8 instances, but if you need more raw control, then the v8 bindings that Deno also made should work.

1 Like

This question is going to sound basic:

So it is pretty much agreed that performance wise,

"JS on V8" beats "JS -> wasm on wasm runtime" ?

1 Like

I'm almost positive. V8 is one of the most optimized JavaScript runtimes there is, and there's a lot more room for optimization when you're targeting native code than when you're targeting WASM, which has to be converted to native code by a separate WASM runtime, and that isn't as expressive or well-featured as raw x86_64.

Now I think eventually there will be a way to compile JS Code ( not the JS runtime ) to WASM, which might be a good option at that point, but I don't think there's a popular way to do that yet.

1 Like

I have no idea but I have read here and there, sorry can't find a link, that it is impossible to compile JS to WASM (or any actual machine code for that matter). It seemed to involve d the idea that JS is just far too dynamic to compile to a fixed sequence of machine instructions. So a run time interpreter/JIT is required.

Edit: On searching for a JS to WASM compiler I find :frowning:

Javy: Run your JavaScript on WebAssembly. Javy takes your JavaScript code, and executes it in a WebAssembly embedded JavaScript runtime.....Javy is currently used for the beta Shopify Scripts platform. GitHub - Shopify/javy: JS to WebAssembly toolchain

And it's written in Rust. Looks interesting.....

1 Like

You asked for JS. If using Lua is an option, you may have a look at the crate sandkiste_lua, which I created. I added limiting the instruction count to defend against while true do end and also use a custom allocator to limit memory usage.

1 Like

This is slightly off-topic, but the question actually came up for me recently, and somebody linked me to this:

2 Likes

More information about why it's not quite as difficult to make JS faster on WASM as you might intuitively assume in this article from the BytecodeAlliance

The tl;dr is that you can anticipate some common JS patterns and include fast paths for them in the original WASM. Sort of like a JIT but with a limited number of optimized chunks decided on in advance. Rather than collecting run time information and compiling JS functions to optimized machine code, you just substitute in optimized chunks when they apply.

4 Likes

Wow, really good blog post it looks like. I'll have to read it carefully sometime. Thanks for the link :+1:

1 Like

If the only requirements were server side + untrusted user scripts, I'd agree that Lua is a superior choice.

In my case, however, I'm looking for scripts that can run both client side (n Chrome) and server side (some Rust container) -- and for this, JS beats out Lua due to Chrome support for JS.

1 Like

Perhaps lua.vm.js could overcome this limitation?

Not really, I was referring to things like tooling support: Chrome Dev Console, other builtin code editors, etc ...

Well I never.

I thought that Deno was the new node.js but designed to isolate/sandbox the JS from the users system. I have not looked into Deno much but I understood that JS under Deno had no access to anything on the machine unless the user granted permissions for it. Deno advertises itself as " The easiest, most secure JavaScript runtime. https://deno.land/

That seems to take care of attempts at malicious system calls and the like. Not sure what to do about the while(1) problem. My understanding is in general the problem automatically detecting whether a program hangs in an endless loop or not is called "The Halting Problem" and that Turing proved in 1936 it could not be done. https://en.wikipedia.org/wiki/Halting_problem/. That only leaves the crude technique of putting time limits on anything you are running.

As for the "while(1) { malloc(..); }" I don't know if Deno caters for setting limits on memory usage. I'm sure it can be done by the Linux kernel on a process by process basis.

You are asking for some way to run sandboxed Javascript from Rust. As it happens Deno is written in Rust. Sounds like you could hack it to do exactly what you want. If not it has some kind of FFI that might get what you want done.Foreign Function Interface | Manual | Deno

Again, I know nothing of the details of any of this so no idea if it is good fit for the problem or not. Having looked into it a bit more today it sounds like something I would find very useful. Thanks for giving me th motivation to look!

1 Like

AssemblyScript could be a good option, as long as you're fine with a more limited support for JS features.

AssemblyScript looks interesting. So we start with TypeScript, force type sigs on everything, remove some of the more dynamic features of JS -- and how we have something that can compile to wasm?

[And, despite the word 'Assembly' in the name, the on-wasm runtime has a GC builtin ?]

What is tooling support for this like? Does anything that support TypeScript suport AssemblyScript, or do we need separate tooling for AssemblyScript ?

There's some form of garbage collection, but I think it's simple. Because the language is simplified compared to JavaScript it doesn't need to be as complicated. ( I'm not even sure that they support closures. )

I haven't looked deep into that aspect of it.

There's an assemblyscript compiler. That compiler can compile itself to WASM, so you can embed the compiler in a browser if you want, or embed it in Rust using a WASM runtime, which is really slick.

Since AssemblyScript doesn't have a lot of JavaScript/TypeScript's features, I think only some things written in TypeScript will work on AssemblyScript.

1 Like

Are you referring to Using the compiler | The AssemblyScript Book

in particular the section of:

<script async src="https://cdn.jsdelivr.net/npm/es-module-shims@1/dist/es-module-shims.js"></script>
<script type="importmap">
{
  "imports": {
    "binaryen": "https://cdn.jsdelivr.net/npm/binaryen@x.x.x/index.js",
    "long": "https://cdn.jsdelivr.net/npm/long@x.x.x/index.js",
    "assemblyscript": "https://cdn.jsdelivr.net/npm/assemblyscript@x.x.x/dist/assemblyscript.js",
    "assemblyscript/asc": "https://cdn.jsdelivr.net/npm/assemblyscript@x.x.x/dist/asc.js"
  }
}
</script>
<script type="module">
import asc from "assemblyscript/asc";
...
</script>

or is there something else even cooler I should be looking for?