Reputation: 8292
I have a situation with some code where eval()
came up as a possible solution. Now I have never had to use eval()
before but, I have come across plenty of information about the potential danger it can cause. That said, I'm very wary about using it.
My situation is that I have input being given by a user:
datamap = input('Provide some data here: ')
Where datamap
needs to be a dictionary. I searched around and found that eval()
could work this out. I thought that I might be able to check the type of the input before trying to use the data and that would be a viable security precaution.
datamap = eval(input('Provide some data here: ')
if not isinstance(datamap, dict):
return
I read through the docs and I am still unclear if this would be safe or not. Does eval evaluate the data as soon as its entered or after the datamap
variable is called?
Is the ast
module's .literal_eval()
the only safe option?
Upvotes: 316
Views: 501761
Reputation: 4287
In recent Python3 ast.literal_eval() "no longer parses simple strings"*, instead you are supposed to use the ast.parse() method to create an AST then interpret it.
I wrote this answer some time ago and I used the "simple string" phrase from my reference at the time, sadly I don't recall the source but it was probably getting outdated, but it is true that at one time this method expected something other than a string. So at the time this was a reference to Python 2 and that fact changed slightly in Python 3, but it does come with limitations. Then at some point I updated the code presented from Py2 to Py3 syntax, causing the confusion.
I hope this answer is still a complete example of how to write a safe parser that can evaluate arbitrary expressions under the control of the author that can then be used to interpret uncontrolled data by sanitising each parameter. Comments appreciated as I still use something similar in live projects!
So really the only update is that for very simple Python expressions ast.iteral_eval(str: statements)
is now, if I understand correctly, regarded as safe.
And this answer is I hope still a working minimal example of how to implement something similar to ast.literal_eval(str: statements)
for a greater diversity of functions, methods and datatypes but still in a simple way that can be considered safe. I am sure there are others methods but that would be out of context as unrelated to the topic of this question.
Here is a complete example of using ast.parse() correctly in Python 3.6+ to evaluate simple arithmetic expressions safely.
import ast
import logging
import math
import operator
logger = logging.getLogger(__name__)
def safe_eval(s):
def checkmath(x, *args):
if x not in [x for x in dir(math) if "__" not in x]:
msg = f"Unknown func {x}()"
raise SyntaxError(msg)
fun = getattr(math, x)
return fun(*args)
bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.Call: checkmath,
ast.BinOp: ast.BinOp,
}
un_ops = {
ast.USub: operator.neg,
ast.UAdd: operator.pos,
ast.UnaryOp: ast.UnaryOp,
}
ops = tuple(bin_ops) + tuple(un_ops)
tree = ast.parse(s, mode="eval")
def _eval(node):
if isinstance(node, ast.Expression):
logger.debug("Expr")
return _eval(node.body)
if isinstance(node, ast.Constant):
logger.info("Const")
return node.value
if isinstance(node, ast.BinOp):
logger.debug("BinOp")
left = _eval(node.left) if isinstance(node.left, ops) else node.left.value
if isinstance(node.right, ops):
right = _eval(node.right)
else:
right = node.right.value
return bin_ops[type(node.op)](left, right)
if isinstance(node, ast.UnaryOp):
logger.debug("UpOp")
if isinstance(node.operand, ops):
operand = _eval(node.operand)
else:
operand = node.operand.value
return un_ops[type(node.op)](operand)
if isinstance(node, ast.Call):
args = [_eval(x) for x in node.args]
return checkmath(node.func.id, *args)
msg = f"Bad syntax, {type(node)}"
raise SyntaxError(msg)
return _eval(tree)
if __name__ == "__main__":
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
logger.addHandler(ch)
assert safe_eval("1+1") == 2
assert safe_eval("1+-5") == -4
assert safe_eval("-1") == -1
assert safe_eval("-+1") == -1
assert safe_eval("(100*10)+6") == 1006
assert safe_eval("100*(10+6)") == 1600
assert safe_eval("2**4") == 2**4
assert safe_eval("sqrt(16)+1") == math.sqrt(16) + 1
assert safe_eval("1.2345 * 10") == 1.2345 * 10
print("Tests pass")
Upvotes: 17
Reputation: 2138
If all you need is a user provided dictionary, a possible better solution is json.loads
. The main limitation is that JSON dicts ("objects") require string keys. Also you can only provide literal data, but that is also the case for ast.literal_eval
.
Upvotes: 9
Reputation: 179392
Python's eager in its evaluation, so eval(input(...))
(Python 3) will evaluate the user's input as soon as it hits the eval
, regardless of what you do with the data afterwards. Therefore, this is not safe, especially when you eval
user input.
Use ast.literal_eval
.
As an example, entering this at the prompt could be very bad for you:
__import__('os').system('rm -rf /a-path-you-really-care-about')
Upvotes: 58
Reputation: 298106
ast.literal_eval()
only considers a small subset of Python's syntax to be valid:
The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and
None
.
Passing __import__('os').system('rm -rf /a-path-you-really-care-about')
into ast.literal_eval()
will raise an error, but eval()
will happily delete your files.
Since it looks like you're only letting the user input a plain dictionary, use ast.literal_eval()
. It safely does what you want and nothing more.
Upvotes: 168
Reputation: 32300
datamap = eval(input('Provide some data here: '))
means that you actually evaluate the code before you deem it to be unsafe or not. It evaluates the code as soon as the function is called. See also the dangers of eval
.
ast.literal_eval
raises an exception if the input isn't a valid Python datatype, so the code won't be executed if it's not.
Use ast.literal_eval
whenever you need eval
. You shouldn't usually evaluate literal Python statements.
Upvotes: 339
Reputation: 1160
eval:
This is very powerful, but is also very dangerous if you accept strings to evaluate from untrusted input. Suppose the string being evaluated is "os.system('rm -rf /')" ? It will really start deleting all the files on your computer.
ast.literal_eval:
Safely evaluate an expression node or a string containing a Python literal or container display. The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, None, bytes and sets.
Syntax:
eval(expression, globals=None, locals=None)
import ast
ast.literal_eval(node_or_string)
Example:
# python 2.x - doesn't accept operators in string format
import ast
ast.literal_eval('[1, 2, 3]') # output: [1, 2, 3]
ast.literal_eval('1+1') # output: ValueError: malformed string
# python 3.0 -3.6
import ast
ast.literal_eval("1+1") # output : 2
ast.literal_eval("{'a': 2, 'b': 3, 3:'xyz'}") # output : {'a': 2, 'b': 3, 3:'xyz'}
# type dictionary
ast.literal_eval("",{}) # output : Syntax Error required only one parameter
ast.literal_eval("__import__('os').system('rm -rf /')") # output : error
eval("__import__('os').system('rm -rf /')")
# output : start deleting all the files on your computer.
# restricting using global and local variables
eval("__import__('os').system('rm -rf /')",{'__builtins__':{}},{})
# output : Error due to blocked imports by passing '__builtins__':{} in global
# But still eval is not safe. we can access and break the code as given below
s = """
(lambda fc=(
lambda n: [
c for c in
().__class__.__bases__[0].__subclasses__()
if c.__name__ == n
][0]
):
fc("function")(
fc("code")(
0,0,0,0,"KABOOM",(),(),(),"","",0,""
),{}
)()
)()
"""
eval(s, {'__builtins__':{}})
In the above code ().__class__.__bases__[0]
nothing but object itself.
Now we instantiated all the subclasses, here our main enter code here
objective is to find one class named n from it.
We need to code
object and function
object from instantiated subclasses. This is an alternative way from CPython
to access subclasses of object and attach the system.
From python 3.7 ast.literal_eval() is now stricter. Addition and subtraction of arbitrary numbers are no longer allowed. link
Upvotes: 92