Andy
Andy

Reputation: 50570

How to safely use exec() in Python?

I have been tasked with building an application where an end user can have custom rules to evaluate whether a returned query results in a warning or alert (based on there own thresholds).

I've built a way for the user to template their logic. An example looks like this:

if (abs(<<21>>) >= abs(<<22>>)):
    retVal = <<21>>
else:
    retVal = <<22>>

The <<21>> and <<22>> parameters will be substituted with values found earlier in the program. Once all this substitution occurs I have a very simple if/else block (in this example) that looks like this stored in a variable (execCd):

if (abs(22.0) >= abs(-162.0)):
    retVal = 22.0
else:
    retVal = -162.0

This will exec() correctly. Now, how can I secure this? I've looked at this article: http://lybniz2.sourceforge.net/safeeval.html

My code ends up looking like this:

safe_list = ['math','acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'cosh', 'de grees', 'e', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'hypot', 'ldexp', 'log', 'log10', 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh'] 
safe_dict = dict([ (k, locals().get(k, None)) for k in safe_list ]) 
safe_dict['abs'] = abs
exec(execCd,{"__builtins__":None},safe_dict)

However, the exec fails when I have the second and third parameter with this exception - NameError: name 'retVal' is not defined

Some of the custom logic the end users have is extensive and much of this changes on a fairly regular basis. I don't want to maintain their custom logic and end users want to be able to test various warning/alert threshold logic quickly.

How can I secure this exec statement from unsafe (either intentional or unintentional) code?

Upvotes: 14

Views: 14840

Answers (3)

Morgan Jonasson
Morgan Jonasson

Reputation: 31

This is how I see it from the experience I had - there are two main dangers with exec and eval, inwhich I mean there is a workaround for both of them:

First off, you have source code running within a source code. This means that this code:

eval("mysql_password")

will give the user access to all variables and instances in the entire module. We don't want that. If this was a flask server connected to MySQL doing eval this way is like open up a door to the public to see your passwords. This user just got your MySQL password and managed to not just log in there but to also clear all data and change the password. Congratulations!

Instead, you will have to create a dictionary where you give the users only the essential variables. If you don't want that, you can also just insert an empty dictionary.

eval("mysql_password",{},{}) #this will fail as it should in this case.

Why are there two dictionaries? It's because the first one is for local variables and the other dict for global variables. this means the code in the string will be able to read and write to local dict but only read from global dict.

Now when we have solved that, there is the 2nd issue:

users can use system commands like "open()" or import modules like os to see all the files in the directory and possibly modify them. In addition they may import prebuilt or externally installed libraries and that way take control on the entire server. in this case there are multiple ways we can combat this. First off, eval is more secure than exec because since eval is only one line and has to return a value, it won't be possible to import a library and use that to get unapproved access. Secondly, one thing you can do is to overwrite all the system commands in the scope:

scope = {"open":None,"file":None,...} #include ALL dangerous system functions, especially "open".

then you can make a "safe" eval like this:

eval("open('wpadmin/sql.ini').read()",scope,scope) # this will now failed since we defined "open" as None in the scope.

Unfortunately there is no guarantee or perfect answer to how to make it completely bullet proof, but I would say the way to approach the problem is in defining a limited scope and stick to eval and try your best to avoid exec. The problem with exec is that import statements are possible there, so you would need to write a filter that removes all the import statements then.

A third approach is (if you run on a server), is to tweak the security level of all files and folders in the ftp folder so that the main source code itself can't read or write to these files. that way the user will get "access denied" error when trying to use "open" and stuff.

and whatever you do for this use case, DO NOT have any overpowered libraries that can wreak havoc on the running PC. I'm thinking mainly of libraries like autopy and pyautogui where users all of a sudden can navigate on a server computer and all you see is a mouse moving around by itself. those have to be UNINSTALLED or else, don't use any of them.

Upvotes: 3

Marcin
Marcin

Reputation: 49846

The only safe way to use eval or exec is not to use them.

You do not need to use exec. Instead of building a string to execute, parse it into objects, and use that to drive your code execution.

At its simplest, you can store functions in a dict, and use a string to select the function to call. If you're using python syntax, python provides all the utilities to parse itself, and you should use those.

Upvotes: 6

David Robinson
David Robinson

Reputation: 78610

Your exec statement isn't adding retVal to your local environment, but to the safe_dict dictionary. So you can get it back from there:

execCd = """
if (abs(22.0) >= abs(-162.0)):
    retVal = 22.0
else:
    retVal = -162.0
"""

safe_list = ['math','acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'cosh', 'de grees', 'e', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'hypot', 'ldexp', 'log', 'log10', 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh'] 
safe_dict = dict([ (k, locals().get(k, None)) for k in safe_list ]) 
safe_dict['abs'] = abs
exec(execCd,{"__builtins__":None},safe_dict)
retVal = safe_dict["retVal"]

Upvotes: 7

Related Questions