13Feb
How To Secure Python Flask App Using Bandit
How To Secure Python Flask App Using Bandit

Cybersecurity is a very important aspect of software development. It is a branch of code testing which is integral in the profitability of major tech companies around the world as highlighted in my previous article on static code analysis. Some weeks back while I was looking for ways to test my flask project for possible security vulnerabilities, I stumbled on a tool called Bandit. Bandit is a tool developed to locate and correct security problems in Python code. To do that Bandit analyzes every file, builds an AST from it, and runs suitable plugins to the AST nodes. Once Bandit has completed scanning all of the documents it generates a report.

Bandit is very useful in detecting security issues and was even featured on kali’s blog as one of the best tools in finding common security issues on your project. Now that we know what bandit is, let us look at some advantages of testing our code for security vulnerabilities.

IMPORTANCE OF SECURING OUR CODE

  •  Unsecure code is prone to external threats and compromise of personal information or company secrets that may result in the loss of a considerable amount of money if exploited. Securing our code is therefore important in avoiding this problem.
  •  Unsecure code can also result in damage to the systems of thousands of users utilizing the software. This could also cost the company a lot of money in compensating the affected users.
    Securing our code will also counter this problem.
  • Unsecure code can lead to loss of life and property. Some malicious organizations exploit software and steal user’s data to blackmail them. This could result in users committing suicide or trading their properties to free themselves. Occurrences like this can be avoided by simply producing secure code.
  • We are now aware of why we should secure our code. We can now get our hands dirty by making use of the Bandit tool.

The Bandit Tool

Bandit can be installed on your system using the command line

pip install bandit

Writing and Analyzing our Code

We would now write our python code to collect names and emails of users with flask and storing them in the database using the python flask framework and then analyzing the code for the security vulnerability issues using the bandit tool.

Prerequisites

Python, Flask, SQLAlchemy, and Bandit.

Installing our dependencies with our command shell

$ pip install bandit
$ pip install flask
$ pip install flask-sqlalchemy

# to check if bandit installed successfully
$ bandit --version
$ bandit 1.7.0

Now that we have successfully installed our dependencies we can write our flask APP that we are going to be testing.

 Our Flask App

Let us begin importing the required modules or libraries in our program.

#Importing Libraries
from flask import Flask,jsonify
from flask_restful import  Api, Resource
from flask_sqlalchemy import SQLAlchemy

Next, we initialize our flask and configure our database using our app config and SQLAlchemy. Our database filename would be data.sqlite3

app = Flask(__name__)
application=app
api = Api(app)
db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.sqlite3'

Now we would be creating our database tables and their structure using ORM(Object Relational Mapping) done with SQLAlchemy, whereas our table names are the class names and the class attributes are the table fields. We then use the command create_all to create our database file.

class User(db.Model):
   id = db.Column('id', db.Integer,primary_key=True)
   name = db.Column('name', db.String(100), nullable=True)
   email = db.Column('email', db.String(100), nullable=True)
   phone = db.Column('phone', db.String(100), nullable=True)

def __init__(self,id,name,email):
   self.id = id
   self.name = name
   self.email = email
   self.phone = phone

db.create_all()

We will now be defining our API endpoint “save” which would save the user’s name and email to the database. Before saving we simply check if the email provided by the user already exists in the database. To save we define an object of the database class “User” and add this object instance to the database.

class save(Resource):
    def get(self):
        name=request.form.get("name")
        if name:
            pass
        else:
            name= request.args.get("name")
        email=request.form.get("email")
        if email:
            pass
        else:
            email=request.args.get("email")
        phone=request.form.get("phone")
        if phone:
            pass
        else:
            phone=request.args.get("phone")
        if User.query.filter_by(email=email).first() or User.query.filter_by(phone=phone).first():
            return jsonify({"success": False, "message": "Email already saved"})
        user = User(name=name,email=email)
        db.session.add(user)
        db.session.commit()

        return jsonify({"success": True, "message": "data saved"})

Finally, we can add a route for the save resource we just created above.

#adding resouce with route
api.add_resource(save, '/save')

if __name__ == '__main__':
    app.run(debug=True)

If you successfully got to this stage without errors, congratulations we can now test for security vulnerabilities with Bandit.

Using Bandit without profiles or baseline

We are going to try out our prospector tool without customizing any configurations for a baseline or profile.

$ bandit collect.py

Here are the results of running bandit on our flask app

 Bandit result on Flaskapp
Bandit result on Flask app.

In the image above, we can see that bandit highlighted an error message “B201:flask_debug_true”. Bandit also explained why this error occurred.This error message is caused by a vulnerability issue that occurs when debug is set to true in the program. When this security issue is present in your code, hackers can simulate errors that will lead to the debug page being displayed and the system secret keys, environment variables, and other important information will be exposed. This will allow hackers to narrow their attack perspective and attack with various techniques most likely capture and decrypt requests using the secret key for Django apps or in our case perform a Dos attack.

Let’s now test bandit on another piece of code so we could see other vulnerabilities Bandit can point out. Below is a view in a python Django code that receives a student’s QR code image and decodes it, then sendS an SMS using Vonage to the parent’s phone number.

def decode_qr(request):
    if request.method == "POST":
        image = request.FILES['snapshot']
        m = qr_test.objects.create(image=image)
        m.save()
        link = "https://school.advancescholar.com/media/" + str(image).replace(" ","")
        r = requests.get(
            "http://api.qrserver.com/v1/read-qr-code/", params={"fileurl": link})
        try:
            qr = r.json()[0]["symbol"][0]
            if qr["error"]:
                data = None
            else:
                data = qr["data"]
        except:
            data = None
        valid = UserProfile.objects.all().filter(name=data, user_type="Student")
        print(data)
        if valid:
            client = nexmo.Client(key='key', secret='secret_key')
            text='Your child '  + data + " arrived school at " + str(datetime.datetime.now())
            client.send_message({
                'from': 'Vonage APIs',
                'to': 'parent_number',
                'text': text,
            })
            return render(request, "qr_code.html", {"message": data + " is a registered student and attendance marked"})
        else:
            return render(request,"qr_code.html", {"message":" Not a registered student"})
    return render(request, "qr_code.html")

Below are the results of running bandit on this piece of code.

bandit result on Django code

bandit result on Django codeIn the image above, we can see that bandit highlighted an error message “B106:hardcoded_password_funcarg”. Bandit also explained why this error occurred. This error message is a result of hardcoding our Venmo secret key to our program. Normally this key could be stored in a separate file that would not be pushed to GitHub. This vulnerability can allow hackers to steal the secret key and gain access to your Venmo app. This allows them to make use of your Venmo account and get details of the app’s use.

Bandit Baseline and Profile Customization

Baseline Customization

Bandit permits specifying the direction of a baseline record to evaluate against the usage of the bottom line argument (i.e. `-b BASELINE` or `–baseline BASELINE`). This is beneficial for ignoring regarded vulnerabilities that you consider as non-issues (e.g. a cleartext password in a unit test). To generate a baseline record clearly run Bandit with the output layout set to JSON (most effective JSON-formatted documents are common as a baseline) and output record course specified:

$ bandit -f json -o PATH_TO_OUTPUT_FILE

Profile Customization

Bandit may be run with profiles. To run Bandit towards the examples listing the usage of handiest the plugins indexed withinside the ShellInjection profile:

$ bandit examples/*.py -p ShellInjection

Vulnerability Testing

Vulnerability testing is a software testing technique carried out to evaluate the magnitude of risks involved in the system in order to reduce the probability of the occurrence of the harmful event.

Bandit supports writing different tests to detect various security issues in your python code.

Vulnerability tests are written in Python and automatically discovered from the plugins directory. Each test can evaluate one or more types of your Python statements. Tests are labeled with the types of Python statements they evaluated (for example: function call, string, import, etc).

Tests are carried out by the BanditNodeVisitor object as it checks every node in the AST. The test results are managed in the Manager and aggregated for output at the completion of a test run through the method output_result from the Manager instance.

However, Bandit contains a list of vulnerability checks to select from. To configure bandit for vulnerabilities you require, you have to edit the YAML config file.

Configuring For Vulnerabilities Tests

Bandit was defined to be configurable and cover a wide number of security needs. This is what makes bandit so special as it enables use either as a  local developer utility or as part of a full CI/CD pipeline.  Like I specified above, bandit vulnerability tests are configurable using the YAML config file.

Here is a list of all bandit vulnerability test plugins available

  • B604: any_other_function_with_shell_equals_true
  • B101: assert_used
  • B102: exec_used
  • B111: execute_with_run_as_root_equals_true
  • B201: flask_debug_true
  • B104: hardcoded_bind_all_interfaces
  • B106: hardcoded_password_funcarg
  • B107: hardcoded_password_default
  • B105: hardcoded_password_string
  • B608: hardcoded_sql_expressions
  • B108: hardcoded_tmp_directory
  • B701: jinja2_autoescape_false
  • B609: linux_commands_wildcard_injection
  • B601: paramiko_calls
  • B109: password_config_option_not_marked_secret
  • B501: request_with_no_cert_validation
  • B103: set_bad_file_permissions
  • B503: ssl_with_bad_defaults
  • B502: ssl_with_bad_version
  • B504: ssl_with_no_version
  • B605: start_process_with_a_shell
  • B606: start_process_with_no_shell
  • B607: start_process_with_partial_path
  • B602: subprocess_popen_with_shell_equals_true
  • B603: subprocess_without_shell_equals_true
  • B112: try_except_continue
  • B110: try_except_pass
  • B702: use_of_mako_templates
  • B505: weak_cryptographic_key
  • B506: yaml_load

In our bandit configurations file, we can decide to choose the specific test plugins we want to run and override the original configurations of the tests. An example of this in our config file is shown below

### profile may optionally select or skip tests

# (optional) list included tests here:
tests: ['B201', 'B301']

# (optional) list skipped tests here:
skips: ['B101', 'B601']

### override settings - used to set settings for plugins to non-default values

any_other_function_with_shell_equals_true:
  no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve,
    os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
    os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, os.startfile]
  shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4,
    popen2.popen2, popen2.popen3, popen2.popen4, popen2.Popen3,
    popen2.Popen4, commands.getoutput,  commands.getstatusoutput]
  subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call,
    subprocess.check_output]

In the configurations file above, the first thing we did was to define the tests we wanted to employ when testing with Bandit. The config “tests: [‘B201’, ‘B301’]” sets bandit to only test our code for these two plugins.  The next thing was to define the test plugins we were going to skip because we were not making use of them. However, if you only wish to control the specific tests that are to be run (and not their parameters) then using -s or -t on the command line may be more appropriate.

The last thing we did was to override the settings set by default on the various test plugins to values we are comfortable with or require to test our code.

NB: If you only want to define the specific tests that are to be run (and not their parameters), use -s or -t on the command line.

In a situation where we need to run several tests for specific functions, we will create several config files and pick from them using the -c subscript.

Handling Just a Line of Code

If you have lines in your code resulting in  errors and you are sure that it is  acceptable, they can be individually silenced by adding # nosec to the line as seen below:

# The following hash is not used in any security context. It is only used
# to generate unique values, collisions are acceptable and "data" is not
# coming from user-generated input
the_hash = md5(data).hexdigest()  # nosec

Auto Generating Our Config File

Bandit-config-generator designed to remove the workload from just configuration. It generates an automatic configuration. The generated configuration will include default config blocks for all detected tests and the blacklist plugin. This data can then be deleted or edited in order to produce a minimal config as desired. The config generator supports -t and -s command-line options to specify a list of test IDs that should be included or excluded respectively. If there are no other options given then the config file generated will not include any tests or skips sections (however, it will provide a curative list of all test IDs to reference when editing).

Configuring our Test Plugins

Bandit configurations file written in YAML format. Options for each plugin test are provided under a section named to match the test method.

For example, given a test plugin called ‘check_if_good’ its configuration section might look like the following:

check_If_good:
  check_typed_exception: True

Writing our Bandit Test

According to the bandit official documentations to write our bandit vulnerability tests we need  to follow the following steps:

  • Identify a vulnerability to build a test for, and create a new file in examples/ that contains one or more cases of that vulnerability.
  • Create a new Python source file to contain your test, you can reference existing tests for examples.
  • Consider the vulnerability you’re testing for, mark the function with one or more of the appropriate decorators:
    @checks(‘Call’)
    @checks(‘Import’, ‘ImportFrom’)
    @checks(‘Str’)
  • Register your plugin using the bandit.plugins entry point, see an example.
  • The function that you create should take a parameter “context” which is an instance of the context class you can query for information about the current element being examined. You can also get the raw AST node for more advanced use cases. Please see the context.py file for more.
  • Extend your Bandit configuration file as needed to support your new test.
  • Execute Bandit against the test file you defined in examples/ and ensure that it detects the vulnerability. Consider variations on how this vulnerability might present itself and extend the example file and the test function accordingly.

These steps are very helpful when trying to write a new test plugin to test your code.

Let’s look at an example of a vulnerability test that was written by following the above steps.

@bandit.checks('Call')
def prohibit_unsafe_deserialization(context):
    if 'unsafe_load' in context.call_function_name_qual:
        return bandit.Issue(
            severity=bandit.HIGH,
            confidence=bandit.HIGH,
            text="Unsafe deserialization detected."
        )

In the example above, a new file was first created in examples/ then a vulnerability to be tested on the code called “prohibit_unsafe_deserialization” was defined. This function checks if unsafe contexts are passed in your code. We are done with the first and second steps in the official documentation. The next step after writing our function is to add the bandit decorator required as seen in step 3 above. For this particular test, the “@call” decorator was used because the context was being called by the function. After adding the decorator, the next step is to register your plugins using the bandit.plugins entry point as stated above. In order to register the test plugin above, there are two methods to adopt;

  • If you’re want to use setuptools directly, you need to add something like the following to your setup call:
# If you have an imaginary bson formatter in the bandit_bson module
# and a function called `formatter`.
entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']}
# Or a check for using mako templates in bandit_mako that
entry_points={'bandit.plugins': ['mako = bandit_mako']}
  • If you’re using pbr, the easier way to do it is to add something like the following to your setup.cfg file:
[entry_points]
bandit.formatters =
    bson = bandit_bson:formatter
bandit.plugins =
    mako = bandit_mako

You can now extend your bandit config file as needed to support the test and finally run bandit against your code to detect security vulnerabilities written and customized by you.

Some Common Plugin Id Groupings

Below are some common vulnerability test plugins in bandit and their descriptions.

B1xx —misc tests
B2xx—application/framework misconfiguration
B3xx—blacklists (calls)
B4xx—blacklists (imports)
B5xx—cryptography
B6xx—injection
B7xx—XSS

To know more about Bandit, visit their official documentation.

Conclusion

  • We learned about Cybersecurity and why it’s important in software development.
  •  We learned about Bandit and why it is useful in detecting simple vulnerability issues.
  •  We learned how to use and customize Bandit in detecting simple security vulnerabilities in our code.
  • We learned how to write and configure our own bandit test plugins and extend them to our config files to test for vulnerabilities in our code.

After reading this article, readers should now be able to understand the importance of securing their code and how to use bandit in analyzing their python codes and projects to detect potential security issues. Thank you for your time and see you soon😃. For any questions don’t hesitate to contact me on Twitter: @LordChuks3.

Resources

One Reply to “How To Secure Python Web App Using Bandit”

  1. I’m new to Python and I don’t know what are setuptools and pbr.
    could you please provide, step-by-step for dummies of how to make my bandit plugin to work?
    Do I need bandit source code from git, or just the CLI installed using pip?
    Where to put all scripts, which directory and absolute/relate location?
    Much appreciate your work!

Leave a Reply