Security scanners for Python and Docker: from code to dependencies

You don’t want to deploy insecure code to production—but it’s easy for mistakes and vulnerabilities to slip through. So you want some way to catch security issues automatically, without having to think about it.

This is where security scanners come in. They won’t solve all your probems—you should still be using services that proactively point out insecure dependencies, for example. But it’s good to have some automated checks in your build or CI system to help catch problems.

For a Python application packaged in Docker, vulnerabilities can occur among other places in:

  1. Your code.
  2. Your code’s Python dependencies.
  3. The system packages (Debian/CentOS/Ubuntu/etc.) included in the Docker image.

Let’s see how you can scan for vulnerabilities in each.

Scanning your Python code

The first place to catch security problems is in the code you’re writing. A useful tool for doing that is Bandit.

Consider the following deliberately insecure code:

import pickle
import sys
from urllib.request import urlopen

obj = pickle.loads(urlopen(sys.argv[1]).read())
print(obj)

If I run bandit against it, it catches a number of problems:

$ bandit example.py
...
>> Issue: [B403:blacklist] Consider possible security implications associated with pickle module.
...
>> Issue: [B301:blacklist] Pickle and modules that wrap it can be unsafe when used to deseria$ize untrusted data, possible security issue.
...
>> Issue: [B310:blacklist] Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected.
...

In the full output each of those warnings also points at the specific line of code where the warning applies. And as one would hope for a tool that needs to run in CI, finding security issues results in exit code 1, which will usually make your build runner fail the build.

When I wrote that example I was only thinking about the fact that pickle is insecure; I wasn’t expecting the URL scheme warning, but it’s a fair point—imagine accepting a URL in a web page form and then opening it. Automated security scanners are handy!

Another tool to look at is pysa, which is included in the Pyre type checker. It can trace values as they flow through your code to see if unsafe inputs are reaching particular functions.

Scanning your Python dependencies

Your Python application likely depends on many Python libraries; occasionally one of them will have a security vulnerability, and you’ll want to make sure you’re using the fixed version.

There are a number of services that will preemptively scan your code and open PRs with suggested updates; GitHub acquired and is integrating Dependabot, for example, so you can use it for free. But you may also want to do some scans in your own CI, as part of your build.

One tool to do that is Safety, from PyUp, one of those services. The free version only gets its vulnerability database updated once a month, and the vulnerability database is only licensed for non-commercial use, so if you want more timely updates or to use it in a commercial project you’ll have to pay.

$ pip install django==1.8
$ pip install safety
$ safety check
...
| package   | installed | affected     | ID       |
+===========+===========+==============+==========+
| django    | 1.8       | <1.11.27     | 37771    |
| django    | 1.8       | <1.8.10      | 33074    |
| django    | 1.8       | <1.8.10      | 33073    |
...
$ pip install --upgrade django
$ safety check
+============================================+
| No known security vulnerabilities found.   |
+============================================+

Again, as one would want in a tool running CI, the exit code is non-zero when security vulnerabilities are found.

Scanning your Docker image

Your Docker image includes not only your Python code and its Python dependencies, but also system packages. For example, if you’re building on my recommended base image, the official python image, your application’s Docker image is based on Debian. Over time, Debian will ship security updates for various included packages, and you want to make sure your image includes those fixes.

Trivy is a command-line tool that lets you scan a Docker image for many kinds of security vulnerabilities, both system packages and programming language-specific packages.

I happened to have an old python:3.7-slim-buster on my machine, so let’s run trivy against it using the less-verbose --light option. If you omit --light you’ll also get a summary of the vulnerability details.

$ trivy --light python:3.7-slim-buster

python:3.7-slim-buster (debian 10.3)
====================================                                                     Total: 102 (UNKNOWN: 0, LOW: 71, MEDIUM: 31, HIGH: 0, CRITICAL: 0)

+----------------|---------------------|---------------+
|    LIBRARY     |  VULNERABILITY ID   | FIXED VERSION |
+----------------|---------------------|---------------+
| apt            | CVE-2020-3810       | 1.8.2.1       |
+                +---------------------|---------------+
|                | CVE-2011-3374       |               |
+----------------|---------------------|---------------+
| bash           | CVE-2019-18276      |               |
+                +---------------------|---------------+
|                | TEMP-0841856-B18BAF |               |
+----------------|---------------------|---------------+
| coreutils      | CVE-2016-2781       |               |
+                +---------------------|---------------+
|                | CVE-2017-18018      |               |

... many more vulnerabilities here ...

102 vulnerabilities is quite a lot, but it’s something of an exaggeration. In particular, there are some security issues you can’t necessarily do anything about:

  • Some of the CVEs in the output above are quite old—one of them is from 2011. A maintainer may choose not to fix certain unimportant security problems.
  • Sometimes you will have security problems that have been reported, but not yet fixed in a released package.

If you want to filter both these categories out, you can use --ignore-fixed.

Finally, if you want a non-zero exit code when vulnerabilities are found (and you do!), you should use --exit-code 1.

Combining all the above:

$ trivy --exit-code 1 --ignore-unfixed --light python:3.7-slim-buster

python:3.7-slim-buster (debian 10.3)
====================================
Total: 4 (UNKNOWN: 0, LOW: 0, MEDIUM: 4, HIGH: 0, CRITICAL: 0)

+---------------|------------------|---------------+
|    LIBRARY    | VULNERABILITY ID | FIXED VERSION |
+---------------|------------------|---------------+
| apt           | CVE-2020-3810    | 1.8.2.1       |
+---------------+                  +               +
| libapt-pkg5.0 |                  |               |
+---------------|------------------|---------------+
| libsystemd0   | CVE-2020-1712    | 241-7~deb10u4 |
+---------------+                  +               +
| libudev1      |                  |               |
+---------------|------------------|---------------+

Now we can see there are just two fixable vulnerabilities in this image, which can be fixed by correctly updating the system packages.

Some caveats about trivy:

  • In theory it supports Python packages as well, but only if you’re using Pipenv or Poetry lock files. Unless you’re using those, don’t rely on its output for Python specifically.
  • Some of its data sources are only licensed for non-commercial use. So you will need to validate the data sources you use are OK for your purposes. I’ve filed an issue to see if this can be documented.

As an alternative to trivy you can use Anchore Engine or Claire + Klar, but unfortunately they’re less easy to setup. In most cases you image registry will also do security scans for you, so you can check those scans are pushing the image.

Setup a scanner today

Setting these scanners up is as easy as adding up a few extra lines to your build or CI configuration. Do that now, and you’ll hopefully get alerted in advance about at least some of the security vulnerabilities in your packaged application.


Learn how to build fast, production-ready Docker images—read the rest of the Docker packaging guide for Python.