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.