On Security and Python's Exec
A recent project at work has renewed my aversion to Python's exec statement--particularly when you want to use it with arbitrary, untrusted code. The project requirements necessitated the use of exec, so I got to do some interesting experiments with it. I've got a few friends who, until I slapped some sense into them, were seemingly big fans of exec (in Django projects, even...). This article is for them and others in the same boat.
Take this example:
#!/usr/bin/env python import sys dirname = '/usr/lib/python2.6/site-packages' print dirname, 'in path?', (dirname in sys.path) exec """import sys dirname = '/usr/lib/python2.6/site-packages' print 'In exec path?', (dirname in sys.path) sys.path.remove(dirname) print 'In exec path?', (dirname in sys.path)""" print dirname, 'in path?', (dirname in sys.path)
Take a second and examine what the script is doing. Done? Great... So, the script first makes sure that a very critical directory is in my PYTHONPATH: /usr/lib/python2.6/site-packages. This is the directory where all of the awesome Python packages, like PIL, lxml, and dozens of others, reside. This is where Python will look for such packages when I try to import and use them in my programs.
Next, a little Python snippet is executed using exec. Let's say this snippet comes from an untrusted source (a visitor to your website, for example). The snippet removes that very important directory from my PYTHONPATH. It might seem like it's relatively safe to do within an exec--maybe it doesn't change the PYTHONPATH that I was using before the exec?
Wrong. The output of this script on my personal system says it all:
$ python bad.py /usr/lib/python2.6/site-packages in path? True In exec path? True In exec path? False /usr/lib/python2.6/site-packages in path? False
From this example, we learn that Python code that is executed using exec runs in the same context as the code that uses exec. This is a critical concept to learn.
Some people might say, "Oh, there's an easy way around that. Give exec its own globals dictionary to work with, and all will be well." Wrong again. Here's a modified version of the above script.
#!/usr/bin/env python import sys dirname = '/usr/lib/python2.6/site-packages' print dirname, 'in path?', (dirname in sys.path) context = {'something': 'This is a special context for the exec'} exec """import sys print something dirname = '/usr/lib/python2.6/site-packages' print 'In exec path?', (dirname in sys.path) sys.path.remove(dirname) print 'In exec path?', (dirname in sys.path)""" in context print dirname, 'in path?', (dirname in sys.path)
And here's the output:
$ python also_bad.py /usr/lib/python2.6/site-packages in path? True This is a special context for the exec In exec path? True In exec path? False /usr/lib/python2.6/site-packages in path? False
How can you get around this glaring risk in the exec statement? One possible solution is to execute the snippet in its own process. Might not be the best way to handle things. Could be the absolute worst solution. But it's a solution, and it works:
#!/usr/bin/env python import multiprocessing import sys def execute_snippet(snippet): exec snippet dirname = '/usr/lib/python2.6/site-packages' print dirname, 'in path?', (dirname in sys.path) snippet = """import sys dirname = '/usr/lib/python2.6/site-packages' print 'In exec path?', (dirname in sys.path) sys.path.remove(dirname) print 'In exec path?', (dirname in sys.path)""" proc = multiprocessing.Process(target=execute_snippet, args=(snippet,)) proc.start() proc.join() print dirname, 'in path?', (dirname in sys.path)
And here comes the output:
$ python better.py /usr/lib/python2.6/site-packages in path? True In exec path? True In exec path? False /usr/lib/python2.6/site-packages in path? True
So the PYTHONPATH is only affected by the sys.path.remove within the process that executes the snippet using exec. The process that spawns the subprocess is unaffected, and can continue with life, happily importing all of those wonderful packages from the site-packages directory. Yay.
With that said, exec isn't always bad. But my personal point of view is basically, "There is probably a better way." Unfortunately for me, that does not hold up in my current situation, and it might not work for your circumstances too. If no one is forcing you to use exec, you might investigate alternatives in all of that free time you've been wondering what to do with.
Comments
Comments powered by Disqus