Open Mobile Menu

Blog

Code Review For Python-Based Web Apps

Views: 9243

Written By: Stephen Haywood July 18, 2016

Recently one of our testers asked if there were any static analysis tools for Python code. He was reviewing a web app written in Python and was trying to identify any high-risk issues. I wasn’t aware of any readily available, security focused static analysis tools for Python because most available tools focus on syntax compliance not security and the few security-focused tools, like Fortify, can be very costly. However, I was able to give him a list of high-risk issues that could be easily identified with a manual review. This list allowed him to check for a number of the high-risk issues on our standard web application checklist.

SQL Injection

Look for any files that contain SQL queries. You want to find queries that are using syntax similar to the following:

     stmt = "SELECT * FROM table WHERE id=?" connection.execute(stmt, (value,))

This means the app is using parameterized queries. You don’t want to find queries that look like either of the following:


     "SELECT * FROM table WHERE id=" + value

     "SELECT * FROM table WHERE id=%s" % value

     "SELECT * FROM table WHERE id={0}".format(value) 

Without some other sanitization in place these will lead to SQL injection. It is better to use the parameterized form.

Command Injection

Look for any files that import the os module. Check these files carefully to ensure that no unsanitized user input is passed to the os.popen*, os.spawn*, os.fork, os.system, or os.exec* methods. Also look for any calls to the popen2 module and the commands module. Keep in mind, with the exception of the os.exec* and os.spawn* methods all of these methods and modules have been deprecated as of Python 2.6 in favor of the subprocess module. Any applications still using these methods or modules should be modified to use the subprocess module instead. While not deprecated, it is recommended to use subprocess instead of the os.spawn* methods.

Look for any files importing the subprocess module and ensure no unsanitized user input is being passed to any of the methods. By default all of the subprocess methods set the shell parameter to False, which prevents shell special characters from being interpreted. This will prevent typical command injection attacks. If the shell parameter needs to be set to True then use the pipes.quote() function or the shlex.quote() function depending on your version of Python to sanitize the user input first.

You don’t want to find calls like this:

     subprocess.call("cat " + user_input, shell=True)

     subprocess.call("cat %s" % user_input, shell=True)

     subprocess.call("cat {0}".format(user_input), shell=True)

If shell=True is needed then use a call like this:

     subprocess.call("cat " + pipes.quote(user_input), shell=True)

     subprocess.call("cat %s" % pipes.quote(user_input), shell=True)

     subprocess.call("cat {0}".format(pipes.quote(user_input)), shell=True)

If shell=True is not needed then these calls will be sufficient, but I would still recommend doing a sanity check on the user input:

     subprocess.call("cat " + user_input)

     subprocess.call("cat %s" % user_input)

     subprocess.call("cat {0}".format(user_input))

Directory Traversal

Look for any files with the open() statement or the os.fdopen() method to ensure unsanitized user input is not passed to these methods.

You want to avoid calls such as:

     open(user_input)

     os.fdopen(user_input)

Before opening a file, check the user’s input and determine if the file is safe to open. Here is an excellent example taken from https://security.openstack.org/guidelines/dg_using-file-paths.html:

    cwd = os.getcwd()

     if os.path.abspath(user_input).startswith(cwd) is True:

         open(user_input)

This will get the absolute path of the users input and ensure it starts with current working directory, like the following:

     >>> os.getcwd()

     '/Users/tester'

     >>> os.path.abspath('../../../etc/passwd')

     '/etc/passwd'

     >>> os.path.abspath('../../../etc/passwd').startswith(os.getcwd())

     False

     >>> 

Depending on how the Python code is structured it may not make sense to use the current working directory. Any directory path can be used.

Cross-Site Scripting

This application was not using a templating language so it was necessary to look for files using the cgi module and ensure all user supplied input was written back to the browser using cgi.escape(). The cgi.escape() method will replace <, >, and & with their HTML equivalents. The first argument to cgi.escape() is the string to perform the escape on. The second argument defaults to False and tells the method whether double quotes should be escaped as well. If the user input will be used inside an attribute then double quotes should be escaped as well.

CGI scripts use simple print statements to create the page returned to the web browser by the web server. Calls like the following should be avoided:

     print(user_input)

Instead use the following:


     print(cgi.escape(user_input))

The difference can be clearly seen in the following example:

     >>> user_input = '<script>alert(1)</script>'

     >>> print(user_input)

     <script>alert(1)</script>


     >>> print(cgi.escape(user_input))

     &lt;script&gt;alert(1)&lt;/script&gt;

>The next example shows the reason for escaping double quotes. In the first print statement notice that the user input adds a new attribute and in the second the escaped quotes prevent the user input from inserting a new attribute.

     >>> user_input = '" onload="alert(1)'

     >>> print('<img src="{0}"'.format(user_input))

     <img src="" onload="alert(1)"


     >>> print('<img src="{0}"'.format(cgi.escape(user_input, True)))

     <img src="&quot; onload=&quot;alert(1)"

Exception Handling

Review the application to ensure appropriate exception handling and logging are taking place. Specifically, look for try/except clauses and ensure the exceptions caught are logged and dealt with appropriately. A common pattern in Python is to use a try/except clause like the following:

     try:

         execute_some_code()

     except:

         pass

This pattern catches all exception and simply ignores them instead of logging and handling them. A more appropriate pattern would look like this:

     try:

         execute_some_code()

         execute_some_other_code()


     except SomeException as e:

         logging.error(e)

         cleanup1()


     except SomeOtherException as e:

         logging.error(e)

         cleanup2()


     except Exception as e:

         logging.error(e)

         cleanup3()

Final Thoughts

In this particular case, the Python web application was written without a framework, which usually protects the developer from making the basic mistakes highlighted above. If you are going to write a Python-based web application, I recommend that you use a good framework like Flask, CherryPy, Web2Py, or Django. Using a framework doesn’t guarantee you will not have trouble with these basic issues but if used properly they prevent most of them.

One other thing to keep in mind, a number of the security issues and their suggested remediations listed above are not limited to web applications. Any Python script used in a production environment should be checked for these issues.

Stephen Haywood

Stephen Haywood, aka AverageSecurityGuy, is a Senior Penetration Tester with AppSec Consulting with 14 years of experience in the Information Technology field working as a programmer, technical trainer, network operations manager, and information security consultant. He holds a Bachelor of Science in Math, the Certified Information Systems Security Professional (CISSP) certification, the Offensive Security Certified Expert (OSCE) certification, and the Offensive Security Certified Professional (OSCP) certification. Over the last eight years, he has helped improve the network security of many small businesses ranging in size from ten employees to hundreds of employees by offering practical, time-tested information security advice.

In his off hours, Stephen created a number of security tools including the Prometheus firewall analysis tool and a set of penetration testing scripts used by testers worldwide. In addition, Stephen has made multiple contributions to the Metasploit exploitation framework including, auxiliary, exploitation, and post exploitation modules. Finally, Stephen created and delivered high-quality security training, spoke at multiple security conferences, and self-published an introduction to penetration testing book.

read more articles by Stephen Haywood