Python eval() Function

In Python, the built-in eval() function evaluates an expression passed as a string argument to it.

For example:

>>> eval('print("Hello world")')
Hello world

The eval() function takes the string argument, converts it into a Python expression, and runs it.

The above code works exactly like this piece of traditional Python code:

>>> print("Hello world")
Hello world

The eval() function can be useful in many ways. However, it is important to understand it poses a security risk to your application. This is because it lets users run hazardous expressions.

In this guide, you are going to learn how to use the eval() function in Python. More importantly, you are going to learn how to mitigate the security issues that are present with this function.

Understanding eval() Function in Python

The generic syntax of the eval function in python

In Python, the eval() function is used with the following syntax:

eval(expression, globals, locals)

Where:

  1. expression is the expression to be evaluated as a string.
  2. globals is an optional argument. It is the global namespace dictionary.
  3. locals an optional argument. It is the local namespace mapping.

To understand what these mean, let’s have a look at the three arguments in more detail.

1. expression

The first argument passed into the eval() function call is the expression. It is a string representation of the expression you want to evaluate. This is the only mandatory argument of the eval() function.

When you call the eval() function, the expression argument is turned into Python code and gets executed.

Here are some examples of calling the eval() function on string-based expressions:

>>> eval("1 + 2")
3

>>> eval("pow(2, 3)")
8

>>> eval("' '.join(['this', 'is', 'a', 'test'])")
'this is a test'

>>> expression = "print('Hello world')"
>>> eval(expression)
Hello world

Notice that the eval() function does not work with statements such as the if statement or a for loop.

For example, let’s try a basic if statement in an eval() function call:

>>> eval("if 1 < 2: print('True')")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    if 1 < 2: print('True')
    ^
SyntaxError: invalid syntax

This results in a SyntaxError.

So you cannot use statements like def, class, if, while, for, import in the eval() function call. Also, it is not possible to assign values in an eval() function expression.

For instance, this line of code causes an error:

>>> eval("number = 100")

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    number = 100
           ^
SyntaxError: invalid syntax

The result is also a SyntaxError. The reason why the assignment is not possible is that an assignment is a statement rather than an expression. And no statements are allowed!

2. globals

The second argument to the eval() function is the globals argument. It is an optional argument that specifies the global namespace for the eval() function. In layman’s terms, this means you can tell which variables are accessible in the expression in the eval( ) function call.

The globals argument is a dictionary that maps the accessible variable names to the actual variables that the eval() function can then use.

For example, let’s specify a variable name and add it to the global namespace of the eval() function:

>>> name = "Alice"
>>> eval('print(f"Hello, {name}")', {"name": name})
Hello, Alice

As another example, let’s see what happens when a variable is left out from the global namespace:

>>> x = 10
>>> y = 20
>>> eval("x + y", {"x": x})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'y' is not defined

As you can see, this results in a NameError that complains about trying to use an undefined variable. If you take a look at the code, you can see that only x was added to the global namespace. Thus y is not available in the expression “x + y”.

You can also define the global namespace directly in the dictionary. This happens by specifying the variable names as the keys and the values as the values.

For instance, let’s define two variables directly into the globals dictionary:

>>> eval("x + y", {"x": 10, "y": 20})
30

If you do not pass the globals argument at all, the eval() function defaults using the current global namespace of the program.

For example:

>>> x = 10
>>> y = 20
>>> eval("x + y")
30

3. locals

The third argument in the eval() function call is locals. It is also an optional argument that is in a form of a dictionary. The locals argument defines the local namespace for the eval() function.

Each Python function has a local scope and namespace. This means variables defined in the function are only accessible inside that function.

In Python, the eval() function is a built-in function that is implemented somewhere under the hood. It has its own local scope that you cannot directly modify from outside.

However, by defining the locals namespace dictionary to the eval() function call, you can extend the local namespace of the eval() function. It is as if you added new variables inside the definition of the eval() function.

In practice, the locals work similarly to the globals.

To specify the locals dictionary into the eval() function call, you need to also have the globals present. It can for example be an empty dictionary.

For instance, let’s define two variables and add them to the local namespace of the eval() function.

>>> x = 10
>>> y = 20
>>> eval("x + y", {}, {"x": x, "y": y})
30

As another example, let’s directly define the local variables in the locals dictionary:

>>> eval("x + y", {}, {"x": 10, "y": 20})
30

Similar to globals, if you do not insert a variable into the locals but try to use it, you get a NameError:

>>> eval("x + y", {}, {"x": 10})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'y' is not defined

When Use the eval() Function in Python?

A small python program that runs code entered by the user.

In Python, it is possible to evaluate string-based expressions using the eval() function. However, it is not possible to run statements or assignments using the eval() function. This might make you think you’ll never need such a function. But there are some use cases worth mentioning.

The main use case for the eval() function is to run dynamic expressions that would normally require more lines of code.

For example, say you’re building a calculator app in which a user enters mathematical expressions. It would be quite a challenge to cover all the possible user inputs with if-else statements or pattern-matching techniques.

This is where the eval() function comes in handy. You can directly evaluate the user’s mathematical expressions.

A very basic implementation would look like this:

expr = input("Enter an expression: ")
result = eval(expr)

print(f"{expr} = {result}")

Here’s an example run:

Enter a mathematical expression: 3 + 8 * 2 ** 3
3 + 8 * 2 ** 3 = 67

This shows how you can use the eval() function and only need three lines of code to create a calculator app.

However, using eval() blindly like this can pose a security threat to your application. In the next section, you are going to learn why eval() can be hazardous and what you can do about it.

Safety Issues with the eval() Function

The eval() function poses a security threat to your application if not handled with care. This is because it allows users to run potentially hazardous code in your application.

Generally, using eval() is against best practices because you cannot know in advance what code is going to be run. However, if you need to use the eval() function, please take the time to learn how to take precautionary measures to minimize the risks.

1. Restrict the globals and locals

Restricting the global and local namespace access in eval function call

You can place restrictions on the global and local namespaces of the eval( ) function with the optional globals and locals arguments.

For example, to restrict access to the global and local scope of the caller, you can pass empty dictionaries to the eval() call.

For example, let’s say there is a secret user ID in the caller’s global scope (that is, in the code):

userid = 12345

expr = input("Enter an expression: ")
result = eval(expr)

print(result)

If a malicious user were able to guess there’s a user ID in the code, they could easily view it by entering the userid as an input to your program:

Enter an expression: userid
12345

This is how easy it is to expose values in code when using the eval() function to run expressions.

To patch this problem, you need to restrict access to the local and global scope. To do this, specify the globals and locals parameters as empty dictionaries in the eval() function call:

userid = 12345

expr = input("Enter an expression: ")
result = eval(expr, {}, {})

print(result)

Now, a hacker fails to get the user ID because the access to the global namespace is restricted:

Enter an expression: userid
> userid

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'userid' is not defined

2. Restrict Installed Modules and Libraries

Restricting the eval function from using modules and libraries

Even with empty globals and locals, it is still possible to access any standard library or third-party module installed on the system that runs the app.

This is possible because when you call eval() with empty globals, a __builtins__ reference is automatically inserted into the globals dictionary. This makes it possible to access any modules and libraries.

This security hole makes it possible to for example run the following hazardous command in the calculator app:

Warning: Don’t run this command on your machine!

__import__('subprocess').getoutput('rm –rf *')

This script deletes every file in the application’s current directory.

To patch the risk, restrict access to the built-in modules and libraries. In other words, you need to make sure the __builtins__ dictionary is empty. To do this, insert an empty __builtins__ dictionary into the globals dictionary:

userid = 12345

expr = input("Enter an expression: ")
result = eval(expr, {"__builtins__": {}}, {})

print(result)

Now trying to access the built-in modules fails too, so there’s not that much a hacker can do anymore.

For example, let’s try to use the math module to calculate the square root:

Enter an expression: __import__("math").sqrt(20)
Traceback (most recent call last):
  File "<string>", line 3, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

Unfortunately even this is not enough to prevent a user from running unwanted code in the app. There is still a quite simple trick that an adversary can pull off to explore restricted areas.

3. Restrict the Input

Even with strict restrictions placed on the global and local namespaces in the eval() function call, a malicious user can still do tricks to access unwanted parts of the code.

In Python, each object has an attribute __class__ that returns the class implementing the object. Furthermore, each class has an attribute __base__ that points to the object class which is the base class of all the Python classes.

For example:

>>> (10).__class__.__base__
<class 'object'>

With access to the object, you can list all the classes that inherit from it:

>>> for sub_class in (10).__class__.__base__.__subclasses__():
...     print(sub_class.__name__)
... 
type
weakref
weakcallableproxy
weakproxy
int
bytearray
bytes
list
NoneType
NotImplementedType
traceback
...

But as you know, running a for loop is not possible in the eval() function call because it is a statement. However, list comprehension is considered an expression in Python. It allows you to turn the above for loop into a one-liner expression that you can run in the eval() function call.

[sub_class for sub_class in (10).__class__.__base__.__subclasses__()]

Now, let’s go back to the calculator app we already protected to some extent.

userid = 12345

expr = input("Enter an expression: ")
result = eval(expr, {"__builtins__": {}}, {})

print(result)

Let’s run the list comprehension to access all the built-in classes in Python:

Enter an expression: [sub_class.__name__ for sub_class in (10).__class__.__base__.__subclasses__()]

['type', 'weakref', 'weakcallableproxy', 'weakproxy', 'int', 'bytearray', 'bytes', 'list', 'NoneType', 'NotImplementedType', 'traceback', ...]

This shows that the malicious user can still go to places you don’t want them to go. Even though listing the classes seems harmless, a hacker can use these classes too, for sure.

For example, let’s demonstrate how a user could call the range() function to create a range of numbers. To do this, the user only needs to know what’s the index of the range function in the list they just printed. In this case, the index of the range function is 12.

So here’s how the hacker could execute the range() function in your calculator app:

Enter an expression: [sub_class for sub_class in ().__class__.__base__.__subclasses__()][12](0, 5)
range(0, 5)

Although this particular action is not dangerous it goes to show how a hacker could still execute code even though it’s seemingly impossible.

So let’s fix this issue too!

One way to tackle the problem is by specifying a dictionary of allowed names that can be used in the expression.

To make this work you need to follow these four steps:

  1. Specify a dictionary of allowed names. This becomes the locals of the eval() call.
  2. Compile the user expression into bytecode.
  3. Verify the bytecode expression only contains allowed names.
  4. Raise an error if a non-allowed word occurs.

Here is an example of a function that implements the above steps to allow only calling the round() function:

def eval_strict(input_expression):
     allowed_names = {"round": round}
     bytecode = compile(input_expression, "<string>", "eval")
     
     for name in bytecode.co_names:
         if name not in allowed_names:
             raise NameError(f"Using {name} is not allowed!")
             
     return eval(code, {"__builtins__": {}}, allowed_names)

Now you can replace the eval() function in the calculator with eval_strict:

expr = input("Enter an expression: ")
result = eval_strict(expr)

print(result)

Now, let’s run this program by summing up two rounded numbers:

Enter an expression: round(3.14) + round(3.141)
> round(3.14) + round(3.141)
6

This succeeds because we allowed using round(). However, let’s try another run where we try to use the abs() function:

Enter an expression: abs(-10) * 3

Traceback (most recent call last):
  File "<string>", line 12, in <module>
File "<string>", line 7, in eval_strict
NameError: Using abs is not allowed!

This fails because the abs() function does not belong to the list of accepted names.

By the way, if you do not want to allow any names to be used at all, leave the allowed_names dictionary empty in the eval_strict function implementation.

Conclusion

Today you learned the basics of the eval() function in Python.

To recap, the eval() function evaluates the expression passed as a string argument to the function. Using eval() can be useful if you want to run expressions dynamically in your program, such as building a calculator app.

However, using the eval() function can be dangerous without proper security measures. This is because it is possible to run hazardous expressions in the application.

Proper safety measures include restricting:

  • The global and local namespaces.
  • Using modules and libraries
  • Accessing the list of built-in classes.

Thanks for reading. Happy coding!

Further Reading

50 Python Interview Questions

References

Leave a Comment

Your email address will not be published. Required fields are marked *