Command Injection in Python:
Examples and Prevention

stackhawk

StackHawk|April 30, 2021

Let’s see what command injection is, how it works in Python, and understand how we can prevent command injection vulnerabilities.

Command injection is one of the less popular injection attacks compared to SQL injection attacks. This is generally because orchestrating one takes more time and consideration. However, overlooking command injection attacks can leave your system or application vulnerable to some big threats. And in some cases, it could even lead to a full system compromise. 

So in this post we will get you familiar with command injection via concrete examples—more precisely, command injection in Python. 

What is Command Injection?

Command injection sends malicious data into an application that can lead to grave damage when dynamically evaluated by the code interpreter. Simply put, this is when an attacker is able to execute commands on your application server via a loophole in your application code. We also call this remote code execution. 

Like other injection attacks, unsanitized user input makes command injection possible. And this is irrespective of the programming language used. We say this because even code written in Python, which has a reputation as a secure programming language, can fall prey to injection attacks. You can do very insecure things with languages designed to be secure. We will see this more clearly in examples covered in the post. 

Examples in Python

command-injection-python-img-1 image

Exploiting Eval() and Exec()

Consider this simple calculator script in Python that takes an expression from the user and evaluates the output. 

Python
compute = input('\nYour expression? => ')
if not compute:
print ("No input")
else:
print ("Result =", eval(comp))

Typing in an expression like 2 * 3 will result in Result = 6 , which is the desired outcome. A malicious user, on the other hand, could type in something like __import__(‘os’).system(‘rm –rf /’)  as input. And this results in a deletion of all files and directories in the script’s folder if the process has enough privileges. 

To prevent such a thing from happening, we need to validate the expressions input by a user. Something like this: 

Python
compute = input('\nYour expression? => ')
if not compute :
print ("No input")
else:
if validate(compute):
print ("Result =", eval(comp))
else:
print ("Error")

In this particular case, our application expects only numbers and arithmetic expressions as input. A good implementation of the validate() function will simply check all the provided input against a white list containing only numeric characters and arithmetic operators. 

Let’s look at an example with the function exec() as well. Consider the following script, which powers a playground that allows beginners to play with simple Python commands they have learned. 

Python
code = input('What command(s) in python did you learn today?')
exec(code)

Inputting something like len(word) gives 4 , and our user is happy. On the other hand, a not-so-friendly user can also type in __import__('os').system.listdir() and see all your directories listed. God knows what they could do from there. But we hope you get the idea of the potential damage. 

Just like with the eval() function, we can get rid of such risk by validating the user input. A good implementation of the validate() function adapted to this script, of course, will suffice to fix this loophole. 

Just like with the eval() function, we can get rid of such risk by validating the user input.

Exploiting Input()

The input() function is the means by which a Python script can read user input into a variable. In Python 2.x, the input() function is equivalent to eval(raw_input). And as we just saw, we must be careful when working with eval(). Consider the script below. Typing in the word “password” as the password will cause the “if” condition evaluating to True. Surprised? Let us see why. 

Python
admin_pass = get_user_pass("admin")
if password == input("Please enter your password"):
  login()
else:
  print "Password is incorrect!"

When we give the word “password” as input, the eval() function will evaluate this into a variable that just happens to match the name of the variable with the actual password. That way, irrespective of the password, someone smart enough to try the word “password” will always gain access to the system. 

Luckily for us, Python 3.x fixes this, and the input() function will always convert the value provided to it into a string. 

OS Commands

Next, we will consider another simple script that pings a provided address. It could be a domain name or an IP address. 

Python
address = request.args.get("address")
cmd = "ping -c 1 %s" % address
subprocess.Popen(cmd, shell=True)

The loophole is glaring, and any command that we put in as an address is executed on the application server. All an attacker has to do is add a semi-column and then put in whatever commands they want. For example, google.com ; ls -la. Fixing this is easy, though, because Python provides built-in functions to remedy this. 

Python
address = request.args.get("address")
command = "ping -c 1 {}".format(address)
args = shlex.split(command)
subprocess.Popen(args)

The shlex.split() function separates the command string into an array before running it. In this way, if there are any malicious inputs, the command execution will fail. 

Preventing Command Injection in Python

A rule of thumb is to avoid using user input in code that is evaluated dynamically. If this is unavoidable, strict user input validation must be put in place to mitigate risk. Nonetheless, there is only so much of this we can do, as human as we are. Hence the need for more practical methods for keeping our applications safe from command injection in Python. Anyone who takes the security of their application seriously adopts these simple and structured methods. 

A rule of thumb is to avoid using user input in code that is evaluated dynamically.

Use Python 3

This could seem like a given to some people, but you might be surprised by the number of Python apps in production still running version 2.7. With the changes that version 3 brings (like the input() function bug fix, we mentioned above), upgrading is not the easiest of tasks, but it is worth the effort. That is why this is the simplest yet the most useful secure practice for building in Python. 

Security Code Reviews

These reviews focus on the general security of the application source code and cover injection vulnerabilities. They are a key step in the life cycle of secure software development, and we commonly refer to the process as Static Application Security Testing (SAST). 

Dynamic Application Security Testing (DAST)

Following Static Application Security Testing we have Dynamic Application Security Testing. This is a modern form of security testing that is more automated and is usually used as part of a CI/CD pipeline. There are a couple of tools out there that allow you to do this, like the one we are building here at StackHawk, and others. What is most important is that you pick a solution that is adapted to your needs and, more important, is one that helps you find and fix vulnerabilities faster. 

Be Up-to-Date with Vulnerabilities

This seems like an impossible one. Most projects make use of lots of open-source projects and packages, and it is practically impossible to stay informed about all the vulnerabilities discovered within each package. Again, you are not alone, because there are tools like Snyk that allow for this. These tools are very complementary to DAST, and you should use both. That way, there is 360-degree security scanning of the application code. 

Wrapping Up

Now that you have a better understanding of command injection in Python, give it a try. If you are a beginner, do some Static Application Security Testing of some code you have recently written and see how well you fare. For more advanced users, pick up an automated application security testing tool and play around with it. StackHawk offers a free plan with unlimited scans and environments that you can use. Hopefully this will add some maturity to your projects. 

This post was written by Boris Bambo. Boris is a backend engineer who’s passionate about cloud-enabled applications and loves using tech to bring ideas to life.


StackHawk  |  April 30, 2021