Python: Securing untrusted scripts/subprocess with chroot and chjail?
Asked Answered
W

2

13

I'm writing a web server based on Python which should be able to execute "plugins" so that functionality can be easily extended.

For this I considered the approach to have a number of folders (one for each plugin) and a number of shell/python scripts in there named after predefined names for different events that can occur.

One example is to have an on_pdf_uploaded.py file which is executed when a PDF is uploaded to the server. To do this I would use Python's subprocess tools.

For convenience and security, this would allow me to use Unix environment variables to provide further information and set the working directory (cwd) of the process so that it can access the right files without having to find their location.

Since the plugin code is coming from an untrusted source, I want to make it as secure as possible. My idea was to execute the code in a subprocess, but put it into a chroot jail with a different user, so that it can't access any other resources on the server.

Unfortunately I couldn't find anything about this, and I wouldn't want to rely on the untrusted script to put itself into a jail.

Furthermore, I can't put the main/calling process into a chroot jail either, since plugin code might be executed in multiple processes at the same time while the server is answering other requests.

So here's the question: How can I execute subprocesses/scripts in a chroot jail with minimum privileges to protect the rest of the server from being damaged by faulty, untrusted code?

Thank you!

Westberry answered 30/7, 2012 at 15:9 Comment(1)
Is this really your job? Shouldn't they know what code they're running? Whatever... Does this help? os.chroot(). Plus, os has got goodies to mess with the uid etc. So, create a new process, (os.fork()?) then os.setuid then os.execle().Anemophilous
B
4

After creating your jail you would call os.chroot from your Python source to go into it. But even then, any shared libraries or module files already opened by the interpreter would still be open, and I have no idea what the consequences of closing those files via os.close would be; I've never tried it.

Even if this works, setting up chroot is a big deal so be sure the benefit is worth the price. In the worst case you would have to ensure that the entire Python runtime with all modules you intend to use, as well as all dependent programs and shared libraries and other files from /bin, /lib etc. are available within each jailed filesystem. And of course, doing this won't protect other types of resources, i.e. network destinations, database.

An alternative could be to read in the untrusted code as a string and then exec code in mynamespace where mynamespace is a dictionary defining only the symbols you want to expose to the untrusted code. This would be sort of a "jail" within the Python VM. You might have to parse the source first looking for things like import statements, unless replacing the built-in __import__ function would intercept that (I'm unsure).

Brewery answered 30/7, 2012 at 16:43 Comment(1)
I went with the dynamic code loading approach because it offers the best flexibility of data interchange and exception handling.Westberry
A
8

Perhaps something like this?

# main.py
subprocess.call(["python", "pluginhandler.py", "plugin", env])

Then,

# pluginhandler.py
os.chroot(chrootpath)
os.setgid(gid) # Important! Set GID first! See comments for details.
os.setuid(uid)
os.execle(programpath, arg1, arg2, ..., env)
# or another subprocess call 
subprocess.call["python", "plugin", env])

EDIT: Wanted to use fork() but I didn't really understand what it did. Looked it up. New code!

# main.py
import os,sys
somevar = someimportantdata
pid = os.fork()
if pid:
    # this is the parent process... do whatever needs to be done as the parent
else:
    # we are the child process... lets do that plugin thing!
    os.setgid(gid) # Important! Set GID first! See comments for details.
    os.setuid(uid)
    os.chroot(chrootpath)
    import untrustworthyplugin
    untrustworthyplugin.run(somevar)
    sys.exit(0)

This was useful and I pretty much just stole that code, so kudos to that guy for a decent example.

Anemophilous answered 30/7, 2012 at 17:5 Comment(3)
This pretty much solved my issues. The thing was tricky because of the installation of psutil within a virtualenv, and user permissions, but after some try/error I fixed it.Neckwear
@habnabit can you elaborate on your edit? I am unfamiliar with why the order matters and I believe it deserves being more explicitly called out in the answer if it has security implications.Anemophilous
you don't close opened fd (so we might be able to recover some right) subprocess remove this aspect. And your first solution is better but consume more memory.Overscore
B
4

After creating your jail you would call os.chroot from your Python source to go into it. But even then, any shared libraries or module files already opened by the interpreter would still be open, and I have no idea what the consequences of closing those files via os.close would be; I've never tried it.

Even if this works, setting up chroot is a big deal so be sure the benefit is worth the price. In the worst case you would have to ensure that the entire Python runtime with all modules you intend to use, as well as all dependent programs and shared libraries and other files from /bin, /lib etc. are available within each jailed filesystem. And of course, doing this won't protect other types of resources, i.e. network destinations, database.

An alternative could be to read in the untrusted code as a string and then exec code in mynamespace where mynamespace is a dictionary defining only the symbols you want to expose to the untrusted code. This would be sort of a "jail" within the Python VM. You might have to parse the source first looking for things like import statements, unless replacing the built-in __import__ function would intercept that (I'm unsure).

Brewery answered 30/7, 2012 at 16:43 Comment(1)
I went with the dynamic code loading approach because it offers the best flexibility of data interchange and exception handling.Westberry

© 2022 - 2024 — McMap. All rights reserved.