Run Python in a sandbox with nsjail

Recently I have been thinking about different ways of building a sandbox environment to run untrusted code. So far, I have been exploring the micro virtual machines approach, which works amazingly well; however, if one needs to run a few lines of Python code, it could be an overkill to spin up a new Linux environment.

As as alternative I decided to try isolation. At the same time there are lots think to consider about it and to name a few:

Luckily, most of those things could be handled by Linux already out-of-the-box. For instance, Linux namespace subsystem, cgroups, and the seccomp-bpf syscall filters of the Linux kernel.

Network exposure is very important unless I shut it down completely. I need to do some extra readings how people sandbox network these days.

Sidenote: a while ago I was solving Google Foobar puzzles, and it occurred to me that Google uses nsjail under the hood to run untrusted Python code. Furthermore, I believe that other companies (e.g. CoderPad) also uses a very similar approach.

Anyway, my point is if someone needs to run a simple project, it could be that she doesn’t need full control over the environment and could get by with some reasonable defaults and preinstalled libraries.

Now, how difficult would it be to implement something similar?

nsjail requires protoc, which wasn’t available for my system, and I had to compile it from sources.

Also, there is no recent nsjail release, and I compiled it from sources too: https://github.com/google/nsjail.

I didn’t write down which dependencies I had to install, so I am leaving this exercise to a reader to figure out. But trust me, it doesn’t require anything fancy.

Now, I can draft a configuration file for nsjail. This particular configuration I borrowed from snekbox project and adjusted to match my environment. Cool project, check it out:

name: "sandbox"
description: "Execute Python"

mode: ONCE
hostname: "sandbox"
cwd: "/sandbox"

time_limit: 6

keep_env: false
envar: "LANG=en_US.UTF-8"
envar: "OMP_NUM_THREADS=1"
envar: "OPENBLAS_NUM_THREADS=1"
envar: "MKL_NUM_THREADS=1"
envar: "VECLIB_MAXIMUM_THREADS=1"
envar: "NUMEXPR_NUM_THREADS=1"
envar: "PYTHONPATH=/usr/lib/python3.8/site-packages/"
envar: "PYTHONIOENCODING=utf-8:strict"

keep_caps: false

rlimit_as: 700

clone_newnet: true
clone_newuser: true
clone_newns: true
clone_newpid: true
clone_newipc: true
clone_newuts: true
clone_newcgroup: true

uidmap {
    inside_id: "65534"
    outside_id: "65534"
}

gidmap {
    inside_id: "65534"
    outside_id: "65534"
}

mount_proc: false

mount {
    src: "/etc/ld.so.cache"
    dst: "/etc/ld.so.cache"
    is_bind: true
    rw: false
}

mount {
    src: "/lib"
    dst: "/lib"
    is_bind: true
    rw: false
}

mount {
    src: "/lib64"
    dst: "/lib64"
    is_bind: true
    rw: false
}

mount {
    src: "/sandbox"
    dst: "/sandbox"
    is_bind: true
    rw: false
}

mount {
    src: "/usr/lib"
    dst: "/usr/lib"
    is_bind: true
    rw: false
}

mount {
    src: "/usr/local/lib"
    dst: "/usr/local/lib"
    is_bind: true
    rw: false
}

mount {
    src: "/usr/bin/python"
    dst: "/usr/local/bin/python"
    is_bind: true
    rw: false
}

mount {
    src: "/usr/bin/python3"
    dst: "/usr/local/bin/python3"
    is_bind: true
    rw: false
}

mount {
    src: "/usr/bin/python3.8"
    dst: "/usr/local/bin/python3.8"
    is_bind: true
    rw: false
}

mount {
    src: "/bin/sh"
    dst: "/bin/sh"
    is_bind: true
    rw: false
}

mount {
    src: "/dev/random"
    dst: "/dev/random"
    is_bind: true
    rw: false
}

cgroup_mem_max: 52428800
cgroup_mem_mount: "/sys/fs/cgroup/memory"

cgroup_pids_max: 1
cgroup_pids_mount: "/sys/fs/cgroup/pids"

iface_no_lo: true

That’s basically it. Now, I can run Python code in a sandbox environement. It is fast, and it is (hopefully) secure. I will have to play with other security policies that nsjail provides to make sure that I’m not missing anything obvious, but so far it is looking promising.

$ cat hello.py 
print('Hello from nsjail!')
$ nsjail --config sandbox.cfg -- python -Su ./hello.py
Hello from nsjail!

I can also run everything inside a Firecracker or Docker instance to make sure that even if someone manages to escape a sandbox she won’t be able to do anything malicious to the host system.

← Run a Firecracker on Nomad

The Pixel Maze: a Pico-8 game →