Hacker News new | past | comments | ask | show | jobs | submit login
Show HN: Run unsafe user generated JavaScript in the browser (workerbox.net)
113 points by turblety on Nov 19, 2022 | hide | past | favorite | 41 comments
I needed a way to let users write JavaScript to create plugins for a site I'm building.

I couldn't find a solution I was happy with, so ended up building one that run's it in a web worker on a separate domain from your main site.

Hopefully I haven't missed anything. If so, please let me know!

My website has an interactive demo you can write code in the browser textarea, and see the output on the right.

Interactive Demo: https://workerbox.net/

Github: https://github.com/markwylde/workerbox




Figma has a great blog post on some different user-generated javascript isolation approaches [1]. They discuss this iframe approach, running a webassembly javascript interpreter (also suggested in these thread comments), and using javascript realms. In the post they originally opted for javascript realms but there's an edit at the top - it seems like they ended up switching to the webassembly javascript interpreter approach.

[1]: https://www.figma.com/blog/how-we-built-the-figma-plugin-sys...


Thanks for sharing the link. That was an amazing read. I originally tried the proxy method (I didn't know about Realms though, they seem cool), and I think I got pretty far. At least, I couldn't find a way to get to the constructor, and access the main scope.

But I'm just not confident you can really isolate code on the same domain that your main code run's in. That's why I ended up moving to a web worker on a different origin. It also comes with the plus that you can kill a hanging web work, as it's run in a different process.

Going to be keeping a close eye on Realm though!


Agoric moved forward and Realms gave way to SES

https://github.com/endojs/endo/tree/master/packages/ses

And Endo is a set of tools (being) built around it to make it more practical for particular usecases


If you need to call into user-generated Javascript synchronously or have greater control over the sandbox environment, you can use WebAssembly to run a Javascript interpreter: https://github.com/justjake/quickjs-emscripten#quickjs-emscr...

QuickJS in WebAssembly is much slower than your browser's native Javascript runtime, but possibly faster than async calls using postMessage. As an added bonus, it can make async functions in the host appear to be synchronous inside the sandbox using asyncify: https://github.com/justjake/quickjs-emscripten#async-on-host...


I believe SharedArrayBuffer and Atomics.wait allow the same synchronous access within the worker to async functions in the host. This is the approach used by PartyTown: https://partytown.builder.io/how-does-partytown-work


I know the industry has moved on to WebAssembly, but I thought it might be worth mentioning that I just recently finished a personal project to compile an arbitrary NodeJS script in QuickJS and call the code from C - all* in a statically or dynamically linked library, no support files.

In case anyone is interested in calling JS from old school C-supported languages, check it out: https://github.com/ijustlovemath/determine-basal-native

It's specific to my application but could easily be reused with a few tweaks.

* Mostly all, the last step is to store the ESM JS script text in an object file and link it in, which I'm still working on.


Nobody has benchmarked postMessage vs QuickJS?


I'll chime in with my random bit of knowledge about a similar topic. Chrome has a bug where if you dynamically create an iframe (document.write(), .createElement(), .innerHTML, etc.), the parent page's Service Worker isn't inherited. It just fails. However, if you use a blank page loaded via the src attribute coming from the same domain, it works as expected. Service workers are really nothing more than client-side network proxies, so once you've loaded that blank page, you can do whatever you want to it (add styles, images, scripts, what have you), then use the service worker to control its access to the network. I use this in an editor app to seamlessly load local assets by intercepting the iframe's network requests. I'm not going for strict security as there's no server-side data involved, so I'm not sure how secure this approach would be for user generated content, I just wanted to toss it out there for those interested. It might save you some time at some point.

(I think it would be wonderful if the Chrome guys fixed this 2+ year old bug so the rest of us don't have to spend a week or so trying to figure out WTF is going on before realizing the obvious-in-retrospect workaround. Just saying.)

https://bugs.chromium.org/p/chromium/issues/detail?id=880768...


The upcoming JavaScript Shadow Realms proposal looks like it solves a similar problem: https://github.com/tc39/proposal-shadowrealm/blob/main/expla...


There's a related proposal for Compartments and Module constructor is a prerequisite to that. A shim for the entire thing exists, with lockdown and Compartments isolating code:

https://github.com/endojs/endo/tree/master/packages/ses

https://github.com/tc39/proposal-compartments/

It has usage already, eg. metamask snaps


Realms look really amazing. I'm going to keep a close eye on this. Stage 3 sounds promising.


Too bad the name sounds idiotic. To even mention it to anyone even in the engineering department is going to make me sound like a super nerd.


but like, your username tho


I did something similar but without needing web workers, iframes, or any type of vm. I used Proxy to intercept Function and hide all scopes outside of the closure. I don't know if it's 100% safe, but it's just for a small game demo, letting users control in-game units in the browser.

https://ai-arena.com/

If anyone knows how to break my game, please let me know!


Once upon a time there was a private retro gaming BitTorrent tracker called UG. They had a few browser games, one of which was blackjack. I wrote a greasemonkey script to play blackjack for me unattended, folks were not amused =(


One upon a time, Crockford worked on ADSafe for a similar goal.

https://www.crockford.com/adsafe/


SES-shim is the modern successor to that by Doug's old pal Mark and team. https://github.com/endojs/endo/tree/master/packages/ses

Also, on its way to being standardized in tc39


You can achieve the same thing with iframes using the "sandbox" attribute.


Another example is the sandbox we have built for Peergos[1] for user apps Combination of double iframe, service worker and writable streams. Not without issue due to browser behaviour. Chrome (...annoyances), Firefox (...feature not necessarily available), Safari (...blocking bugs) [1] https://peergos.org/posts/a-better-web


Oh, dude! Genius. I needed something exactly like this. Is there a similar lightweight approach in node land?

Edit: my usecase is a form builder where users can specify fields that have their values computed from other fields. I've had to hobble it by using templating languages for the custom code, or just let them use javascript and be okay with the risk since the users get their own subdomain, so it's kinda like their own website.


There is vm2 [0] for nodejs, but if you look at the issues there are escapes found occasionally, even some this year. I think it's too risky to run untrusted code on node.

You could run a headless chrome using puppeteer, then run workerbox. But it's probably too much overhead for a server app.

Oh, if you're making a form builder, I have to recommend json-editor [1]. It's not mine and I haven't actually used it just (I've only found really recently), but basically you give a JSONSchema and it will make a form for you, with validations and conditionals all built in. Might come in useful.

0. https://github.com/markwylde/vm2-process

1. https://github.com/jdorn/json-editor


Nice, I'm building a plugin system for my webapp too! I gave up completely on security [0] though because my plugins want DOM access, and may possibly manipulate elements outside their specific control, so I was like "fuck it". Thankfully there's a clear difference between my webapp and website, so hopefully I'll be able to educate my users to not trust stuff on the app side. We'll see :|

Separately, how are you thinking about designing your plugin system? I built a PoC here [1] that basically revolves around combining a DI container with the decorator pattern. We start with an initial default DI container/object, then that container is passed to a plugin which wraps certain methods or adds new ones. That new container is then passed to the next plugin which wraps it, etc, etc. Actual implementation is here [2]. The only thing that sucks about my design is that plugins must call `.bind(this)` when wrapping a method. So basically plugins have unfettered access to my DI container. Perhaps I'm giving plugins too much power? :shrug_shoulders:

0: https://github.com/AlexErrant/Pentive/blob/main/design-decis...

1: https://github.com/AlexErrant/Pentive/blob/main/app/src/plug...

2: https://github.com/AlexErrant/Pentive/blob/main/app/src/plug...


I'm not too sure right now about the design of the plugin system, but I'm thinking it'll just be setting a bunch of JSON data, declaring how elements/menus/pages will behave. Still need to work on this part.


This came really handy, as I was in need for a way to run sandboxed user code in a coding game!

Another cool thing with WebWorkers is that the code is run in a separate thread and allows you to terminate it if it runs into an infinite loop, for instance.


This is actually what the Azure Portal does [1] for embedding 3rd party applications. They even developed a custom HTML language to describe how to layout applications to give a consistent look.

The downside was the portal started to become slow, and sometimes would have 20+ iframes open.

[1] https://github.com/Azure/portaldocs/blob/main/portal-sdk/gen...


Ahh really interesting. Yeah I think (hope?) one benefit of using a web worker should be if anyone writes some blocking code, it will not hold up the main web page thread. Also, as web workers can be terminated, if they run too long the main thread can just kill it.

I'll have to read up more about the portal-sdk to see if there's any other stuff on there that could be helpful building a plugin system. Thanks.


What advantages does workerbox have over existing solutions like jailed[1]?

[1] https://github.com/asvd/jailed



Endojs/SES is the latest and if you look at the list of things it has to do to make JS safe, you’d feel nervous about any approach that doesn’t use an interpreter and process isolation.

Side channel attacks against CPU caches are still a real issue in JS.


Please note a function like `fetch` still works, and might be privacy (and security!) issues in this method.


Really awesome!! Just this week I was wondering how I was going to approach this for a project of mine. I recalled the figma article but your approach seems simpler. I’ll definitely give this a shot! Thank you!!


Wow. The article did not match the expectations I got from the title. I thought this was a joke that allows you to run dangerous code that browsers would normally block by default or something.


> in a web worker on a separate domain

Does it need a separate domain for every script to prevent two or more user generated scripts from influencing each other?


It's a really good question, and I originally [0] actually had some code that would randomise the sub domain. You can still do this if you host it yourself.

But I think it's probably not nessisary so long as your users are not using indexDB (or any other data storage on that domain), as there should be no other way for the web workers to communicate with each other.

0. https://github.com/markwylde/workerbox/blob/master/lib/index...


But have you tried compiling V8 to WebAssembly to run the JS inside a VM within JS? /s


It's actually possible to do it with a library called Duktape [0].

But yeah, I get it would be super slow, and create a huge wasm file for the client to download.

0. https://duktape.org/


I believe Figma does this. Considering the lack of options it ain't so bad. Other options include running an iframe, or using something like `ses` library. I do both depending on the plugin. As always the biggest pita is sharing instances of anything at all sigh.


What's the difference between safe and unsafe JavaScript?


In this context, whether it's written by you or by the user. Never trust user input.


unsafe javascript exists.


open invite for security researchers to test this app?




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: