Safe/secure way to load plugins into web app
Asked Answered
M

3

6

I have a web app that accepts JS plugins. That is, JavaScript code that someone else wrote that the user wants to load into my app.

Currently I am using eval() to evaluate their JS code into the runtime, but I know thats not secure. Is there a better method than eval() for doing this?

To be clear, the user is pointing me to a dry text file (a URL), and that JS in the file needs to come to life somehow.

There's only two ways I know of dynamically importing a JS script:

  1. Use AJAX, get the JS code, then run eval() on it.
  2. Dynamically add a <script> tag to the DOM

The purpose of the question is to figure out if one is more secure than the other or if there is a better way than the above 2 options.

Midwest answered 15/7, 2018 at 5:44 Comment(10)
It depends on what you need to allow those plugins to do. But the only totally secure method is to run them in a sandboxed frame and provide access to your API via messaging (which can be wrapped in ES6 Proxy for elegancy).Susansusana
There's nothing wrong with eval, it's exactly the tool you'd use to make code from a text file come to life. The insecure thing is your general idea of running code written by untrusted persons in your app. You can solve your problem by making sure that everyone trusts the authors of the code - or you can tell us more details about what you need the "plugins" to do so that we can suggest a more secure solution than loading arbitrary javascript.Plonk
@OlegzandrDenman As it stands, the question is much too broad to be answerable. You'd probably better post your own question where you can put a bounty and supply more details yourself, instead of reviving this months-old question by someone else.Plonk
@Plonk well now that there is a bounty I updated it to be more clear, I am still looking for a definitive answer I just put this on the backburner but would like to keep it aliveMidwest
There are two ways I can think of, that will make it secure: 1. Create your own programming language, which you compile into the JavaScript. That way you can be sure that only trusted things can happen. 2. Use the user contributed code only in an iframe which only can communicate by a postMessage driven API. Both approaches are not trivial, I admitPycnidium
@rakim Is there a reason why this is tagged google-chrome-extension? Because I don't see anything relevant in the question.Ordonnance
@Ordonnance yes this is primarily for a Chrome ExtensionMidwest
Take a look at github.com/dtao/lemming.jsLashoh
@Cryptopat so lemming.js uses a web-worker to do the evaluation? I guess the web-worker is in it's own isolated scope? I think using new Function() would be better?Midwest
That is the whole point, the isolated scope. It is much more secure this way. This said it really depend! With new Function() the whole window object will be readable and modifiable (it is actually an xss hole). One alternative, with same downsides, make the script as a blob and inject as a full script: https://mcmap.net/q/394045/-alternatives-for-javascript-eval-duplicateLashoh
P
4

Is one of these two more secure than the other?

No, they're equally bad (good) from a security perspective.

They differ in details that would lead to different approaches in making them more secure, but ultimately both do run code written by an untrusted third party in your environment, with all its privileges. It's basically a persisted XSS issue.

Is there a better way than the above 2 options?

Many. It depends mostly on what those plugins should do in your application, who writes them and who installs (enables) them. Neither you as the application provider nor the user wants arbitrary code run havoc on the user's data. If the plugins need to access the data, you need administrative measures to ensure that only trusted code will run, like plugin code audits. At least you will need to inform your users that they must trust the plugin authors before enabling the plugin, which puts the burden on them. Also you should ensure to have usable logs in case something went wrong.

If you really want to run arbitrary, untrusted code without giving it access to user data, you will want to consider sandboxing. There are various approaches that essentially do the execution in a virtual machine that the code cannot break out from.

Plonk answered 16/12, 2018 at 15:38 Comment(0)
A
4

For Chrome Extensions I would specifically use sandboxingEval which allows the loading of sandboxed files that can be accessed within an iframe that the extension hosts. The only message passing would be through normal iframe messaging.

For example, declare within the manifest.json which page to sandbox:

{
  ...
  "sandbox": {
    "pages": [
      "plugin.html"
    ]
    "content_security_policy":
        "sandbox allow-scripts; script-src 'self' https://plugin.com/"
  ],
  ...
}

Make sure the external domain is white-listed so it could be embedded. In the CSP policy, allow-scripts is there if embedding <scripts> is needed.

Now within the sandboxed page plugin.html, do anything with the external script. In this case, the external plugin is downloaded, and passing messages back to the extension process through messaging.

<!doctype html>
<html>
  <head>
    <script src="https://plugin.com/mohamedmansour/plugin.js"></script>
  </head>
  <body>
    <script>
      // Whatever my plugin contract is, lets send something back to our extension
      // that the plugin initialized.
      Plugin.do.something.here(() => {
          window.postMessage({
              name: 'CustomInitEvent',
              data: 'initializing'
          }, *);
      });

      // Listen from your extension plugin.html page some events.
      window.addEventListener('message', (event) => {
        var command = event.data.command;

        switch(command) {
          case 'CustomCommandA':
            event.source.postMessage({
              command: 'CustomCommandHello',
              data: 'pong command a'
            }, event.origin);
            break;
        }
      });
    </script>
  </body>
</html>

Now within the popup or anywhere, just embed the plugin.html. In this case, popup.html looks like this

<html>
  <head>
    <script src="plugin-manager.js"></script>
  </head>
  <body>
    <iframe id="theFrame" src="plugin.html"></iframe>
  </body>
</html>

Then your plugin-manager.js is responsible of controlling plugin.

const iframe = document.getElementById('theFrame');
window.addEventListener('message', function(event) {
  switch(event.name) {
    case 'CustomInitEvent':
      console.log('Plugin Initialized');
      break;

    case 'CustomCommandHello':
      console.log('Hey!');
      break;
  }
});

iframe.contentWindow.postMessage({
   command: 'CustomCommandA'
});

iframe.contentWindow.postMessage({
   command: 'CustomCommandB'
});

Something along those lines. If dynamic plugins is what is needed, just add query parameters to the iframe. Within plugin.html, just dynamically add the script element, and just call it this way:

<iframe id="theFrame" src="plugin.html?id=121212"></iframe>
Amimia answered 19/12, 2018 at 20:38 Comment(0)
U
2

One option you might want to consider is running the plug-in in a WebAssembly instance. This has a few benefits:

  1. It has many very strong sandboxing guarantees https://webassembly.org/docs/security/ and is designed to run untrusted code.
  2. The plug-in can be in any language, not just javascript, including much more performant languages like rust, go, c, zig, etc. Your javascript app doesn't need to know or care what language the plug-in was written in. Creating and running plug-ins here is very fast.
  3. The plug-ins can run on the backend as well. You can run them anywhere really as there are now WebAssembly runtimes everywhere.

The downside is that wrangling all this low level technology can be a little tricky. That's where I'd recommend an open source project like Extism to help smooth over some of the lower level details: https://extism.org/.

Edit: note I am a contributor to Extism

Unmeet answered 18/9, 2024 at 21:13 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.