How to secure Python Flask applications
Flask is a powerful, lightweight, and versatile web framework for Python, that's designed to make it easy for developers to develop web applications quickly with minimal boilerplate code. It's a stand-alone microframework that doesn't need any additional libraries or tools and has no database abstraction layer.
Flask includes features like routing, template rendering, and request handling, and follows the Web Server Gateway Interface (WSGI) standard — which is known for its simplicity and flexibility.
Unfortunately, just like any other web framework, Flask is susceptible to vulnerabilities if it's not properly secured. The most common security risks for Flask include cross-site scripting (XSS), cross-site request forgery (CSRF), and SQL injection.
You can easily see on the Snyk Vulnerability Database some of the many security vulnerabilities found for the Python flash library:
In this article, you'll learn about some best practices related to securing Python applications built with the Flask web application framework. You'll start by looking at some insecure configuration examples and then learn how to mitigate and fix any issues.
What is Web Server Gateway Interface (WSGI) standard?
WSGI allows for a standardized way for web servers to communicate with web applications written in Python. It's an intermediary layer that enables web servers to forward requests to a web application or framework and then deliver responses back to a client.
Why is WSGI important?
Before WSGI, Python web frameworks and servers often had specific coupling, meaning a particular web framework could only run on certain web servers. WSGI broke this limitation, enabling more flexibility and interoperability.
How does WSGI compare to similar concepts in other languages?
WSGI is similar in purpose to the Java Servlet API in Java or Rack in Ruby. These technologies abstract the details of HTTP requests and responses, allowing developers to focus on writing web application logic rather than dealing with underlying server communication.
Insecure configurations for Python Flask applications
Following are some of the most common insecure configuration issues that impact Python Flask applications and that developers should keenly ensure they aren’t repeating these mistakes in their code bases:
Secret key exposure
Unintentionally disclosing the secret key is a common security lapse involving sensitive information, such as the Flask secret key, API key, and passwords, within the source code. Integrating confidential details directly into the source code poses a substantial security risk, as vicious actors can exploit this vulnerability to gain unauthorized access to various services.
If you've accidentally hard-coded your secret key and API key in the source code, it will look like this:
# Insecure Code
api_key = "my_sensitive_api_key"
api_url = "https://api.example.com"
SEC_KEY = "your_secret_key"
# use of secret key
app.config["SECRET_KEY"] = SEC_KEY
# usage of the API key
response = requests.get(api_url, headers={"Authorization": f"Bearer {api_key}"})
Tip: this is a good time to install the Snyk extension in your IDE so that you get a visual indication of secret key exposure like the blue squiggly line you can see in my editor:
In this instance, to mitigate secret key exposure, you need to generate the secret key securely. Although there are several ways to generate the secret key, one of the most common ways is to use the uuid module. The uuid
module is primarily used for generating universally unique identifiers (UUIDs) based on the principles defined in RFC 4122. This module generates a 32-byte random hexadecimal string that can be used as a secret key.
To generate a secure key, you import the uuid
module in your code and use the uuid4().hex
functionality:
import uuid
uuid.uuid4().hex
However, generating the secret key is not enough. You also need to use it securely in your Flask app. The recommended secure way is to assign the secret key to an environment variable
or use an encrypted configuration file and store the key there. If you choose to assign the secret key to an environment variable
, it will look like this:
import os
# Better Code
api_key = os.environ.get("MY_API_KEY")
api_url = "https://api.example.com"
SEC_KEY = os.environ.get("MY_SEC_KEY")
# use of secret key
app.config["SECRET_KEY"] = SEC_KEY
# Usage of the API key
response = requests.get(api_url, headers={"Authorization": f"Bearer {api_key}"})
Developers should follow configuration conventions such as those recommended in the 12 factor app to use environment variables for storing their credentials to avoid data leak, unauthorized access and potential misuse.
Debug Mode Enabled
The debug mode is generally intended for development environments, providing detailed error messages and comprehensive information about the application's state. However, enabling this mode in a production environment exposes sensitive information and the internal workings of the system.
As an example, in this Flask web application, the debug mode is enabled in production:
# Insecure code
from flask import Flask
appFlask = Flask(__name__)
@appFlask.route('/home')
def home():
result = "Testing Snyk"
return 'example of debug mode enabled'
if __name__ == "__main__":
appFlask.run(debug=True)
Here, the debug mode is set to True, which means if any error occurs in production, the sensitive information, including usernames and passwords, can be exposed in the debug console.
In this scenario, it's very easy to forget that you set the debug mode to True
and forget to change it before deploying your application to production. That's why it's best to use a platform like Snyk that can help you find and fix the vulnerabilities in your code and applications. Snyk supports a wide range of programming languages, including Python, Go, PHP, JavaScript and others.
In addition, Snyk can be easily integrated with various IDEs, including Visual Studio Code and PyCharm, as well as CI pipelines, such as Jenkins, CircleCI, and Maven, and workflows.
For instance, to use Snyk in this debugging scenario, all you need to do is search for "Snyk" in Visual Studio Code and install it:
Once installed, Snyk will appear in the panel on the left. Go ahead and log in or create a Snyk account, if you don't already have one, by clicking the Connect button. Then a browser window will appear that looks like this:
Once connected, navigate back to Visual Studio Code and open the folder where your Flask script is stored. You should now be able to see any vulnerabilities in your code:
As you can see, Snyk found that the debug mode was still enabled. To mitigate this issue, you need to secure your Flask application by setting the debug mode to False
:
# Secure Code
from flask import Flask
appFlask = Flask(__name__)
@appFlask.route('/home')
def home():
result = "Testing Snyk"
return 'example of debug mode enabled'
if __name__ == "__main__":
appFlask.run(debug=False)
In addition to disabling the debug mode in production, you should also use the logging mechanism to collect the logs from the application to check for possible issues in the code.
Unprotected sensitive data in configuration files
Storing critical information (e.g, database credentials, API keys, and encryption keys) in configuration files is a common practice in Flask applications. However, the data can be compromised if you leave these files unprotected.
Consider the following scenario where your configuration file (usually config.ini
) holds the credentials to connect to the database:
# Insecure Config
[database]
username = admin
password = my_secure_password
Here, the username and password are mentioned in plaintext, so if an attacker gains access to this file, they can use the credentials to gain access to sensitive information.
The best way to resolve this issue is to avoid configuration files and use environment variables to save your credentials. Since environment variables collect all the required information in a hidden file, like .bash_profile
, you can simply gitignore
this file to avoid accidentally pushing it to version control platforms, like Git or Bitbucket, which are easily accessed by attackers.
If you're in a scenario where you need to use the configuration files in your Flask app, you need to make sure that sensitive data is encrypted:
# Secure Config
[database]
username = admin
password = $encrypted_password
Here, a placeholder or an encrypted value is used to represent the password. In this scenario, sensitive data can be accessed at runtime using an external secure key management system or a safe decryption technique.
Flask application vulnerabilities
Now that you know a little bit more about some insecure configurations for Python Flask applications, it's time to look at some common web security vulnerabilities that can also affect Flask applications.
Cross-site scripting (XSS)
An XSS is a web security vulnerability where an attacker tries to inject arbitrary JavaScript or HTML code into the web page. This allows attackers to execute malicious scripts in a user's browser.
Jinja2, a template used by Flask, provides an Auto Escape feature to handle XSS vulnerabilities. By default, all the variables are auto-escaped (ie they don't contain any HTML component until told otherwise). This means any input passed by the user containing HTML or JavaScript code is safely escaped by Jinja2, preventing the execution of malicious scripts.
If auto-escaping is set to False
, the Flask application is prone to XSS vulnerabilities. For instance, here, you have a web application that requires input from the user to render a page:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/')
def index():
user_input = request.args.get('input')
return render_template('index.html', input=user_input)
The corresponding index.html
page where auto-escaping is set to false
would look like this:
{% autoescape false %}
XSS Example
User Input: {{ input }}
{% endautoescape %}
If an attacker injects the malicious script through the input
parameter, it executes when the page is rendered, leading to potential security risks, such as data theft, account takeover, and phishing attacks.
To mitigate XSS, developers need to configure their website so that it can properly escape the text and does not include any HTML component. All you need to do is set the auto-escaping to true
to prevent the Flask application from an XSS attack:
{% autoescape true %}
XSS Example
User Input: {{ input }}
{% endautoescape %}
Another method to prevent XSS is to use explicit markup escaping. To implement explicit markup escaping, you need to use the |safe
filter on a web page, like this:
XSS Example
User Input: {{ input|safe }}
However, using |safe
should be done with great care as it assumes that the content being marked as safe is free from any malicious script.
To learn more about cross-site scripting vulnerabilities in Python, head over to a byte-sized security lesson at Snyk Learn.
Cross-site request forgery (CSRF)
CSRF is a malicious attack that deceives a victim into submitting a malicious request. In a web application, CSRF involves an attacker making an unauthorized request to a web page on behalf of the authorized user. You often see this type of attack on platforms like Facebook, where someone sends an image to a group, and if users in that group click on the image, their account gets deleted.
To better understand CSRF, consider a simple state-changing application that works without CSRF protection, like this:
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/change_password', methods=['POST'])
def change_password():
new_password = request.form.get('new_password')
# Change password logic here
return redirect(url_for('index'))
This code submits a form to change the password with a POST request. Since there's no CSRF security, an attacker can trick users into changing their password by directing them to a different website that contains a crafted HTML or script. A form that doesn't contain CSRF protection looks something like this:
CSRF Example
New Password:
Change Password
To mitigate CSRF vulnerabilities, you need to use the Flask-WTF extension, along with the CSRFProtect
middleware. Flask-WTF is a Flask plugin that incorporates the WTForms library, which offers practical functionality for easily generating and managing forms for a Flask web application.
You can install it using pip as follows:
pip install Flask-WTF
Once installed, you can update your Flask application with Flask-WTF for CSRF protection:
from flask import Flask, render_template, request, redirect, url_for
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key' # Set a secret key for CSRF protection
csrf = CSRFProtect(app)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/change_password', methods=['POST'])
def change_password():
new_password = request.form.get('new_password')
# Change password logic here
return redirect(url_for('index'))
Here, CSRFProtect
is initialized with the Flask app, and the SECRET_KEY
is set. This key is specifically used to create and validate CSRF tokens.
Don't forget to update the template (index.html
) to include the CSRF token, like this:
CSRF Example
{{ csrf\_token() }}
New Password:
Change Password
This csrf_token()
function generates a hidden input field that contains the CSRF token. When the form is submitted, the token is validated by Flask-WTF to ensure that the request is not a CSRF attack.
FSQL injection and database security
Now, look at a few different Flask issues related to database security:
SQL Injection
SQL injection is a serious issue where an attacker executes arbitrary SQL code on a database by manipulating the parameters used in an SQL query. This issue can lead to unauthorized access, data manipulation, and disclosure of sensitive information.
For Flask applications, if the inputs are not properly sanitized before being used in an SQL query, an SQL injection attack can occur.
To better understand this attack, consider a sample Flask code that selects the data from a database, like this:
from flask import Flask, request, render_template
import sqlite3
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# Vulnerable SQL query
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
# Execute the query
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute(query)
user = cursor.fetchone()
conn.close()
if user:
return 'Login successful'
else:
return 'Login failed'
This query expects the username and password in the query string. If an attacker injects inputs, like ' OR '1'='1'; --
, the query becomes like this:
SELECT * FROM users WHERE username = '' OR '1'='1'; --' AND password = '';
Since this query always results in True, the attacker would be logged in and can have unauthorized access to the data.
Identifying code that is prone to SQL injection is difficult, but thankfully, Snyk can help. Since you already configured Snyk in Visual Studio Code, all you need to do is open the script, and Snyk will auto-detect the SQL injection issue:
An easy way to mitigate this SQL injection attack is to use parameterized queries or statements. In
Flask, you can also use the execute
method of the cursor object to bind parameters securely. A parametrized query looks like this:
from flask import Flask, request, render_template
import sqlite3
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# Secure SQL query using parameterized query
query = "SELECT * FROM users WHERE username = ? AND password = ?"
# Execute the query with parameters
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute(query, (username, password))
user = cursor.fetchone()
conn.close()
if user:
return 'Login successful'
else:
return 'Login failed'
In this code, ?
placeholders are used, whose actual values are provided as the tuple in the execute method, ensuring that user inputs are treated as data and not as executable SQL code.
Make sure you always validate and sanitize user inputs to avoid this type of attack. You can sanitize the input with the help of techniques like escape characters, third-party libraries, and regular expressions.
For an indepth informative article about SQL injections I recommend reading SQL injection cheat sheet: 8 best practices to prevent SQL injection attacks.
Authentication and authorization
Authentication and authorization are critical components of web security. A breach in authentication and authorization can lead to several security issues, which you'll learn about next.
Authentication security issues
One of the most common authentication security issues is the brute-force attack, where an attacker repeatedly tries to guess a username and password to gain access to a certain website or service. Another common issue is session fixation, where the attacker sets the session ID to a known value, possibly through a phishing attack.
To mitigate brute-force attacks, you need to implement a session lockout mechanism. In this approach, the user is locked out after a certain number of failed login attempts:
from flask import Flask, request, session
app = Flask(__name__)
app.config['MAX_LOGIN_ATTEMPTS'] = 3
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# Check if the user is locked out
if 'login_attempts' in session and session['login_attempts'] >= app.config['MAX_LOGIN_ATTEMPTS']:
return 'Account locked. Please try again later.'
# Validate the username and password
# ...
# Update login attempts
if 'login_attempts' in session:
session['login_attempts'] += 1
else:
session['login_attempts'] = 1
# Authenticate the user
# ...
In this code, you define the MAX_LOGIN_ATTEMPTS
as 3
, which means if someone tries to enter the wrong credentials more than three times, they'll be locked out.
In the login()
method, you get the username
and password
and check if the user is locked out. If not, you go ahead and check the login attempts. If the credentials are correct, then you're good to go. But if not, the login attempt increases by one, and the process repeats until the user is successfully logged in or locked out.
In addition to a session lockout mechanism, you should also use strong password policies and consider using multifactor authentication (MFA).
To mitigate the session fixation issues, you can regenerate session IDs after a user logs in or after a certain period (as auto-expiry) even if the user continues to stay logged in:
from flask import Flask, request, session
app = Flask(__name__)
app.secret_key = 'your_secret_key'
@app.route('/login', methods=['POST'])
def login():
# Authenticate the user
# ...
# Regenerate session ID after successful login
session.regenerate()
# ...
In this code, the login()
method regenerates the session ID using the regenerate()
method once the user is successfully logged in.
Authorization security issues
When a web app is developed, every feature isn't intended for every user (ie a specific user should have access to only a select set of functionalities). When the roles are not properly assigned to users, unauthorized access can occur, which is why you need role-based access control (RBAC).
It's also possible for authenticated users to access unauthorized sources by manipulating parameters or URLs, which is often referred to as insecure direct object references (IDOR). I highly recommended reading this post on finding and fixing insecure direct object references in Python for more information.
To resolve RBAC and IDOR issues, you need to assign specific user roles and enforce access controls based on those roles, like this:
from flask import Flask, request, session
app = Flask(__name__)
@app.route('/admin_dashboard')
def admin_dashboard():
if 'role' in session and session['role'] == 'admin':
# User has admin role and is authorized to access the admin dashboard
# ...
return 'Admin dashboard page'
else:
# User is not authorized
return 'Unauthorized access'
In this code, the admin_dashboard()
method implements the functionalities that should be accessed if the access role is admin. Otherwise, it returns an *Unauthorized access*
warning.
You also need to manage access control so that users can access only the resources they're authorized to:
from flask import Flask, request, session
app = Flask(__name__)
@app.route('/user_profile/')
def user\_profile(user\_id):
if 'user\_id' in session and session['user\_id'] == user\_id:
# User is authorized to access their own profile
# ...
return 'User profile page'
else:
# User is not authorized
return 'Unauthorized access'
You shouldn't store credentials in plaintext because they can be easily accessed by attackers for malicious intent. To prevent this, you should use hashing and salting to store the user credentials.
Hashing transforms a password into a fixed-length string of characters, making it computationally infeasible to reverse the process and obtain the original password. Salting involves adding a unique random value (the salt) to each password before hashing, which helps prevent attacks such as rainbow table attacks.
Outdated libraries and security updates
If you're using outdated Flask libraries, they may contain vulnerabilities that could be exploited by attackers. Libraries often release security updates and patches to address vulnerabilities. You should always make sure you're using the most up-to-date package.
To ensure you're using the latest security patches, you can use tools like pip
or conda
. Additionally, you can use tools like Snyk to scan your entire code for dependencies and infrastructure-as-code (IaC) configurations.
Another best practice is to pin the dependencies in the requirements.txt
file. This ensures that the project uses specific versions, giving developers control over updates and the ability to test for compatibility.
Secure file uploads
If a Flask website gives anyone the ability to upload any kind of file, several issues can occur. For instance, if an attacker uploads malicious files to the server, a denial-of-service (DoS) attack can occur, with the possibility of overwriting existing files. Some executable files can also produce different types of computer viruses.
Following is an example of code that allows any user to upload any files:
from flask import Flask, request, flash, redirect, url_for
from werkzeug.utils import secure_filename
import os
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
@app.route('/upload', methods=['POST'])
def upload_file():
file = request.files['file']
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
flash('File successfully uploaded')
return redirect(url_for('uploaded_file', filename=filename))
In this code, the upload_file()
method reads the file provided by the user as input and stores it in the uploads
folder.
The first solution to avoid insecure files is to define a set of approved file-type extensions. You should never allow executable or script file types in your Flask app.
Additionally, make sure you store the files in a secure location to avoid unauthorized access:
from flask import Flask, request, flash, redirect, url_for
from werkzeug.utils import secure_filename
import os
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['ALLOWED_EXTENSIONS'] = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
flash('File successfully uploaded')
return redirect(url_for('uploaded_file', filename=filename))
else:
flash('Invalid file type')
return redirect(request.url)
Here, multiple conditions have been applied to check the authenticity of the file types.
Another best practice is to randomize the file names to prevent overwriting and minimize the risk of predictable naming conventions:
from flask import Flask, request, flash, redirect, url_for
import os
import uuid
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
def generate_random_filename():
return str(uuid.uuid4())
@app.route('/upload', methods=['POST'])
def upload_file():
# ... (other code)
if file and allowed_file(file.filename):
filename = generate_random_filename()
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# ... (other code)
Error handling and information disclosure
In any kind of application, error handling is important. In production, using the print statements and enabling the debug mode is not advisable. If you use an improper log mechanism, it can reveal several important details about your application.
For instance, displaying the detailed error message in production can reveal sensitive information about the application's internals, making it easier for attackers to identify potential vulnerabilities:
OperationalError: (1045, "Access denied for user 'root'@'localhost' (using password: YES)")
This example reveals the most important information about the database associated with the Flask app.
Additionally, displaying the full trackbacks in error responses can reveal the application's code structure and potentially expose vulnerabilities like this:
File "/path/to/app.py", line 42, in some_function
result = 1 / 0
ZeroDivisionError: division by zero
To mitigate information disclosure through error handling, you can create custom error pages for generic error codes (eg 404 Not Found
and 500 Internal Server Error
) without exposing sensitive details. To do so, you can use the errorhandler
decorator to define custom error handlers:
from flask import Flask, render_template
app = Flask(__name__)
@app.errorhandler(404)
def page_not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(error):
return render_template('500.html'), 500
Moreover, you should implement proper logging for exceptions and errors. Log messages should be detailed enough for debugging but not expose sensitive information:
import logging
from flask import Flask
app = Flask(__name__)
logging.basicConfig(filename='error.log', level=logging.ERROR)
@app.route('/divide')
def divide_by_zero():
try:
result = 1 / 0
return str(result)
except Exception as e:
app.logger.error(f"An error occurred: {str(e)}")
return "Internal Server Error", 500
By following these best practices, you can ensure that your Flask application is robust and secure.
Conclusion
Flask is a popular web framework for small- and medium-scale web applications. However, like any other web development tool or framework, Flask can encounter issues if not handled properly. It's important that you're not only aware of these issues but also able to mitigate them to prevent malicious actors from taking advantage.
You are invited to follow this code repository on GitHub for full working examples of some of the Flask application security topics we discussed.
Snyk can help you easily find and mitigate any vulnerabilities in your code. Snyk supports various programming languages and seamlessly integrates with your tools, pipelines, and workflows. Get started with Synk for free today.
Finally, be sure to follow Snyk’s Python Security Best Practices Cheat Sheet and acquire application security skills in Python through the free Snyk Learn website.