README ¶
Secure JavaScript Sandbox
"Build a simple CLI tool that deploys & runs arbitrary JavaScript files in a secure isolated process or thread."
We want to execute untrusted third party JavaScript code in a secure manner, managing the execution through a command-line interface (CLI).
We use Docker as our sandboxing technology (mechanism for running untrusted code). Code will be ran in isolated container processes, which we will start, clean up and monitor through Docker. Using Docker instead of virtual machines will give faster startup times, though at the cost of some extra security concerns.
We implemented the CLI and sandbox launcher in Go, a cross-platform compiled language similar to C.
We assume that most code will be ran using a Node.js runtime (invoked with node
or npm
). Nevertheless other JavaScript runtimes and engines are supported through Docker images.
Usage
Run a JavaScript file using Node.js and see the results after the sandbox completes running
sjs run test/javascript/hello-world.js
Pass custom command line arguments
sjs run test/javascript/mod-exp.js --args "65 17 3233"
Run a file that processes data using a third party API (requires Internet access)
sjs run test/javascript/derive-age.js --args "'{ \"name\": \"tomas\" }'"
Run an Express.js web server with npm dependencies that provides temporary access to a user's data
sjs run test/javascript/temporary-link \
--cmd="/bin/bash -c \"npm install . && npm run start -- '{'name':'ElonMusk', 'email': 'elon.at.boringcompany.com', 'dateOfBirth': '110001' }'\"" \
--ports "3000:3005" \
--env PORT=3005 \
--env HOST=0.0.0.0
Use curl localhost:3000/mydata
to get your data.
The web server shuts down after the first request.
Note that a custom start command, ports to publish, and environment variables can be supplied.
Development
System Design
Command-line interface
We use the third party Kong command-line parser for parsing subcommands and flags. This module provides advantages over Go's flag
module, such as making it easier to have a complex CLI similar to Docker without much effort, which is what we are after.
Sandboxing
We pull the specified image. We create a Docker container, overriding the start command to execute the scripts. We copy the untrusted user files to the Read-Write layer of the Docker container. We start the container, thus executing the user's files.
Alternatively we could create a new image, with the user's files as the top new layer. This way we could re-run the containers easily from an image.
Code layout
We follow https://github.com/golang-standards/project-layout for our Go project structure.
Testing
For small testable functions that do branching we write Go unit tests using the testing
package.
For carrying out integration tests (CLI with Docker from the user's point of view) we use BATS, a bash testing tool. This allows us to run commands in bash and check that processes have a non-error exit status and have certain output in stdout.
Security
Greater security precautions must be made for docker than for virtual machines.
All containers without a --network specified, are attached to the default bridge network. This can be a risk, as unrelated stacks/services/containers are then able to communicate.
For example running the temporary-link
web server in two sandboxes with the default network, it is possible to shell into one of the containers and send a request to another container's web server.
Thus for each container we create a random bridge network, so that no two containers should share the same network and be able to push data to one another.
Alternatively we can run the docker daemon with --icc=false
(disable inter-container communication).
Containers shouldn't run as a root user.
We specify our user as "node". Extra permissions should be granted using "--cap-add".
The docker daemon needs root access and thus can be dangerous. The daemon can be run in experimental rootless mode.
Management of docker installation can be performed using Ansible, i.e. https://github.com/konstruktoid/ansible-docker-rootless can install rootless docker. If connecting to a remote docker daemon, configure the connection to use SSH or TLS. The underlying operating system can be further protected with Linux Security Modules (i.e. AppArmor).
Images can be verified before being pulled and run.
This becomes important once the user can specify which image they want, i.e. which Node version or which programming environment.
Future enhancements
- Improve cmd and args shell character escaping. Right now it's not possible to pass valid JSON via command-line arguments (see the Express.js example). Double quotes must be escaped. The whole expression is hard to read.
- Real time feed of stdout and stderr from sandboxes.
- Sandbox management from the CLI - list, delete, stop, logs, clone, edit, environment.
- More security precautions. Automated security tests. Docker daemon configuration