280

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?

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
tijko
  • 7,599
  • 11
  • 44
  • 64

6 Answers6

299

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.

Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
Volatility
  • 31,232
  • 10
  • 80
  • 89
  • 41
    This isn't 100% correct advice since any bitwise operators (or overloaded operators) will fail. Eg. `ast.literal_eval("1 & 1")` will throw an error but `eval("1 & 1")` will not. – Daniel van Flymen May 22 '17 at 00:34
  • 1
    Just curious. Shouldn't we use expression parsers or something if we're expecting something like "1 & 1" ? – thelinuxer Jan 18 '18 at 00:52
  • 1
    @thelinuxer you still should, yes; you just wouldn't be able to use `ast.literal_eval` for something like that (e.g. you could implement a parser manually). – Volatility Jan 18 '18 at 05:53
  • 14
    @DanielvanFlymen - to me, your example shows that this *is* good advice. When you don't want operators (like `&`) to be evaluated, you use `literal_eval`. The fact that you can't put arbitrary code there to be executed is a feature, not a bug. – Ken Williams Nov 10 '21 at 22:06
156

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.

Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
Blender
  • 289,723
  • 53
  • 439
  • 496
85

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 hereobjective 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

Sunit Gautam
  • 5,495
  • 2
  • 18
  • 31
Kiran Kumar Kotari
  • 1,078
  • 1
  • 10
  • 24
  • 1
    i am using python 2.7 and i just checked its working fine on python 3.x. My bad i kept trying it on python 2.7 – Mourya Sep 23 '16 at 07:18
  • 4
    `ast.literal_eval("1+1")` does not work in python 3.7 and as said before, literal_eval should be limited to literals of those few data structures. It should not be able to parse a binary operation. – Sesshu Oct 03 '18 at 12:52
  • Could you explain your `KABOOM` code, please? Found it here: [`KABOOM`](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) – winklerrr Jan 04 '19 at 08:57
  • 3
    @winklerrr `KABOOM` is nicely explained here: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html – Elijas Dapšauskas Feb 18 '19 at 15:32
  • `ast.literal_eval("1+1")` raises `ValueError: malformed node or string on line 1` on python 3.10. – adrin Sep 11 '22 at 10:27
56

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')
nneonneo
  • 171,345
  • 36
  • 312
  • 383
14

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.


* UPDATE (Q1:2023): I sometimes get comments about the meaning of "simple strings" in this context. On reading up on the current status I've added this update to try and addresses that.

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, operator, math
import logging

logger = logging.getLogger(__file__)

def safe_eval(s):

    def checkmath(x, *args):
        if x not in [x for x in dir(math) if not "__" in x]:
            raise SyntaxError(f"Unknown func {x}()")
        fun = getattr(math, x)
        return fun(*args)

    binOps = {
        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,
    }

    unOps = {
        ast.USub: operator.neg,
        ast.UAdd: operator.pos,
        ast.UnaryOp: ast.UnaryOp,
    }

    ops = tuple(binOps) + tuple(unOps)

    tree = ast.parse(s, mode='eval')

    def _eval(node):
        if isinstance(node, ast.Expression):
            logger.debug("Expr")
            return _eval(node.body)
        elif isinstance(node, ast.Str):
            logger.debug("Str")
            return node.s
        elif isinstance(node, ast.Num):
            logger.debug("Num")
            return node.value
        elif isinstance(node, ast.Constant):
            logger.info("Const")
            return node.value
        elif isinstance(node, ast.BinOp):
            logger.debug("BinOp")
            if isinstance(node.left, ops):
                left = _eval(node.left)
            else:
                left = node.left.value
            if isinstance(node.right, ops):
                right = _eval(node.right)
            else:
                right = node.right.value
            return binOps[type(node.op)](left, right)
        elif isinstance(node, ast.UnaryOp):
            logger.debug("UpOp")
            if isinstance(node.operand, ops):
                operand = _eval(node.operand)
            else:
                operand = node.operand.value
            return unOps[type(node.op)](operand)
        elif isinstance(node, ast.Call):
            args = [_eval(x) for x in node.args]
            r = checkmath(node.func.id, *args)
            return r
        else:
            raise SyntaxError(f"Bad syntax, {type(node)}")

    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")
Jay M
  • 3,736
  • 1
  • 24
  • 33
  • What if I wanna parse a ast.Lambda, say safe_eval("lambda x: x * 2")? Many thanks – Menglong Li Dec 27 '21 at 06:19
  • The post is specifically about simple arithmetic evaluation without parsing code, not about parsing Python syntax. If I can do "lambda x: x * 2". Then I could possibly do "lambda x: format_hdd()". Anyway to answer your question, where X is a variable, use safe_eval("X * 2".replace("X", "55")) In my actual application I use f-string like syntax, e.g. safe_eval(f"{X} * 2") – Jay M Dec 30 '21 at 17:09
  • What do you mean by "no longer parses simple strings"? AFAIK, its behaviour hasn't changed across Python versions, like if you look at [the docs for 3.0](https://docs.python.org/3.0/library/ast.html#ast.literal_eval), the description is pretty much the same. It never allowed parsing arbitrary expressions, if that's what you mean, and it never stopped parsing strings, if that's what you mean. – wjandrea Feb 05 '23 at 20:42
  • @wjandrea I started in Python 1.x (can't recall what the x was) When revisting old code I found the issue. IIRC, the words are exactly as written in the PEP that removed "simple strings" due to that being a vector for miscreants. I agree I should have linked that refernce when I wrote the answer. But that was some time back. Not sure I could find it again, but I will try for completeness. In any case the answer is still a valid example of how to write a simple interpreter to evaluate expression data, that you have no control over, without compromising security. – Jay M Feb 24 '23 at 13:28
  • @JayM But what does "simple strings" mean? – wjandrea Feb 24 '23 at 14:50
  • As I said, I recall that was the words in the explanation I came across of why my old code had to be re-written. I did not link that reference as I assumed that the change was a well known one. I'd not used AST for several years until I needed to parse user supplied Verilog files if you a curious. Obviously I did not want a user putting anything in their macros that could be misused. The old code would have from Py2 or perhaps even late Py1. I really don't remember. – Jay M Feb 27 '23 at 11:59
  • @wjandrea I've undated the answer in a way that should answer your question – Jay M Apr 14 '23 at 14:05
  • @JayM: I think you are confused about the "simple string" thing. `ast.literal_eval` was added in 2.6, and did not exist in 1.x. [Since 2.6](https://docs.python.org/release/2.6/library/ast.html#ast.literal_eval), `ast.literal_eval` has taken a `node_or_string`, which has remained unchanged all the way to 3.11. – nneonneo Aug 02 '23 at 17:13
9

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.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Chinasaur
  • 2,108
  • 2
  • 16
  • 17